Adobe Launch Nested AEM Component Click Tracking
Overview
When my company began using AEM Components, it was the MarTech team’s job to implement click tracking on those components. We already had global click tracking in place that would take the data-track-noun
and data-track-verb
attributes, concatenate them, and delimit them with a colon. But this was going to require additional complexity.
For starters, the AEM Components were going to be nested inside each other. There could be several layers of nesting. And when a component was clicked, we were going to want to concatenate the click tracking values of the parent components all the way down to the child that was clicked.
For example, the Careers page has three sections:
Employment
Apprenticeships
Internships
Within each of these sections there is a set of cards, each representing a category. For example, the “Employment” section contains these cards:
Administration
Research & Development
Sales
Installation & Repair
Support
And each card contains two buttons:
Learn More
View Jobs
If a user clicks a “Learn More” button and all I pass is learn-more
, that won’t tell our analysts much about which careers are getting the most clicks. Instead, they want values like:
employment:research-development:learn-more
employment:installation-repair:view-jobs
employment:administration:learn-more
This was going to require more than just the standard two data-track-noun
and data-track-verb
attributes.
Solution
I decided that we could add a data-track-component-level-X
attribute, with X
being a number. (These would have to be set dynamically, because we would never know how many levels a given component was going to be nested.)
data-track-component-level-1="employment"
data-track-noun="research-development"
data-track-verb="learn-more"
The three (or more) of those would then be concatenated, in the correct order, and only if they exist. Since there could be multiple levels of parent components, we could potentially wind up with data-track-component-level-1
, 2
, 3
, etc.
The question was how to add these attributes to each AEM Component. Our Content team did not have the bandwidth to add each one manually. Furthermore, AEM Components might be nested in a multitude of ways, and those ways might be changed on the fly. If each such change involved the complete reauthoring of every AEM Component’s tracking attributes, the scope of each change would become unwieldy, and the opportunity for mistakes would increase.
Therefore, it was decided that these values would be set programmatically. We came up with a two-pronged approach. The first prong was on the AEM side, which would be handled by the Content team:
Each AEM Component would be given a
data-track-component-type
attribute. Its value would represent the type of component:header
,footer
,breadcrumb
,crowdriff
, etc. (This is so we will know which elements to treat as AEM Components.)Values from heading tags, link text, etc. are programmatically copied into
data-track-verb
attributes.Each of these values would get programmatically standardized into lowercase kebab-case.
The second prong would be handled by the MarTech team, via Adobe Launch:
On each page/route load, an Adobe Launch rule would fire that would crawl the page. If it found any elements containing a
data-track-component-type
attribute, it would collect its tracking attributes.Then it would check the next layer of children for that element. If any contained a
data-track-component-type
attribute, the tracking attributes from the parent element would be added to the tracking attributes of the child element using thedata-track-component-level-X
attribute.The process would repeat for the children of that child, until no more children were found. Depending on how many layers of nesting there were for the AEM Components, this is where we might wind up with
data-track-component-level-1
,data-track-component-level-2
,data-track-component-level-3
, etc.Once an element has been modified, the Launch rule would set a flag on it so we know it's been handled. That way, if more AEM Components got added to the page after the initial page load and our rule needed to crawl the page again, it could skip modifying the components it already modified in the previous pass.
Then, the MarTech team would have to modify the Global | Set Variables | Clicks or Events #25
rule. Currently, that rule only checked for data-track-noun
and data-track-verb
and concatenated the two. Now it would have to factor in any number of data-track-component-level-X
attributes at the beginning of that concatenation. (For an explanation on how that was accomplished, see my article on Adobe Launch Global Clicks or Events Tracking.)
But to see how the programming was done for the AEM Component attributes, keep reading.
‘Global | Set AEM Component Data Attributes’ Rule
We created an Adobe Launch rule named Global | Set AEM Component Data Attributes
. Here’s the full syntax for context. (Scroll past it for an explanation on how each part works.)
const aemComponentTracking = {
setTrackingAttributes: (currentElement, mergedTrackingAttributesArr) => {
mergedTrackingAttributesArr = (!!mergedTrackingAttributesArr && Array.isArray(mergedTrackingAttributesArr)) ? mergedTrackingAttributesArr : [];
const numberOfValues = mergedTrackingAttributesArr.length;
if (!!currentElement && numberOfValues > 0) {
if (numberOfValues === 1) {
currentElement.removeAttribute('data-track-noun');
}
mergedTrackingAttributesArr.forEach((trackingAttribute, i) => {
switch (i) {
case numberOfValues - 1:
currentElement.setAttribute('data-track-verb', trackingAttribute);
break;
case numberOfValues - 2:
currentElement.setAttribute('data-track-noun', trackingAttribute);
break;
default:
currentElement.setAttribute(`data-track-component-level-${i + 1}`, trackingAttribute);
}
});
currentElement.setAttribute('data-track-component-attributes-set', 'true');
}
aemComponentTracking.checkChildren(currentElement, mergedTrackingAttributesArr);
},
uniqueValue(arr, str) {
let unique = true;
if (!!arr && Array.isArray(arr) && arr.length > 0 && !!str && arr.includes(str)) {
unique = false;
}
return unique;
},
mergeTrackingAttributes: (currentElement, existingTrackingAttributes, parentTrackingAttributes) => {
existingTrackingAttributes = (!!existingTrackingAttributes && Array.isArray(existingTrackingAttributes)) ? existingTrackingAttributes : [];
parentTrackingAttributes = (!!parentTrackingAttributes && Array.isArray(parentTrackingAttributes)) ? parentTrackingAttributes : [];
mergedTrackingAttributesArr = [];
const mergedTrackingAttributesArrPusher = (arr) => {
if (!!arr && Array.isArray(arr) && arr.length > 0) {
arr.forEach(arrItem => {
if (aemComponentTracking.uniqueValue(mergedTrackingAttributesArr, arrItem)) {
mergedTrackingAttributesArr.push(arrItem);
}
});
}
}
mergedTrackingAttributesArrPusher(parentTrackingAttributes);
mergedTrackingAttributesArrPusher(existingTrackingAttributes);
aemComponentTracking.setTrackingAttributes(currentElement, mergedTrackingAttributesArr);
},
getTrackingAttributes: (currentElement, parentTrackingAttributes) => {
parentTrackingAttributes = (!!parentTrackingAttributes && Array.isArray(parentTrackingAttributes)) ? parentTrackingAttributes : [];
const existingTrackingAttributes = [];
let level = 1;
let parentComponentLevelX = '';
do {
parentComponentLevelX = currentElement.getAttribute(`data-track-component-level-${level}`);
if (!!parentComponentLevelX) {
existingTrackingAttributes.push(parentComponentLevelX);
}
level++;
} while (!!currentElement.getAttribute(`data-track-component-level-${level}`));
const dataTrackNoun = currentElement.getAttribute('data-track-noun');
if (!!dataTrackNoun && dataTrackNoun !== 'no-title') {
existingTrackingAttributes.push(dataTrackNoun);
}
const dataTrackVerb = currentElement.getAttribute('data-track-verb');
if (!!dataTrackVerb && dataTrackVerb !== 'no-title') {
existingTrackingAttributes.push(dataTrackVerb);
}
const dataTrackComponentAttributesSet = currentElement.getAttribute('data-track-component-attributes-set');
if (
!!dataTrackComponentAttributesSet &&
dataTrackComponentAttributesSet === 'true'
) {
aemComponentTracking.checkChildren(currentElement, existingTrackingAttributes);
} else {
aemComponentTracking.mergeTrackingAttributes(currentElement, existingTrackingAttributes, parentTrackingAttributes);
}
},
checkChildren: (currentElement, parentTrackingAttributes) => {
let currentElementChildren;
parentTrackingAttributes = (!!parentTrackingAttributes && Array.isArray(parentTrackingAttributes)) ? parentTrackingAttributes : [];
if (!!currentElement) {
currentElementChildren = currentElement.children;
if (!!currentElementChildren && currentElementChildren.length > 0) {
currentElementChildren = [...currentElementChildren];
if (Array.isArray(currentElementChildren)) {
currentElementChildren.forEach(currentElementChild => {
if (
currentElementChild.hasAttribute('data-track-component-type') ||
!!['A', 'BUTTON'].includes(currentElementChild.tagName)
) {
aemComponentTracking.getTrackingAttributes(currentElementChild, parentTrackingAttributes);
} else {
aemComponentTracking.checkChildren(currentElementChild, parentTrackingAttributes);
}
});
}
// NOTE: If there were no children, do nothing (stop).
}
}
},
init: () => {
if (
!!window && !!window.document && !!window.document.body &&
!!window.document.body.querySelector('[data-track-component-type]')
) {
aemComponentTracking.checkChildren(window.document.body);
}
const shadowDOMs = _satellite.getVar('shadow doms');
if (!!shadowDOMs && Array.isArray(shadowDOMs) && shadowDOMs.length > 0) {
shadowDOMs.forEach((shadowDOM) => {
if (!!shadowDOM.querySelector('[data-track-component-type]')) {
aemComponentTracking.checkChildren(shadowDOM);
}
});
}
},
}
aemComponentTracking.init();
Explanation
The entire rule is written as an object literal (const aemComponentTracking = {}
), so each method inside that object is declared prior to any other methods that will call it. Therefore, this explanation will work from the bottom up.
Line 131: On the last line of code,
aemComponentTracking.init()
is invoked.Lines 112 - 128:
init
method:Lines 113 - 118: The first thing
init
does is check the regular DOM.Line 115: To reduce performance drag, a quick check is done to make sure there is at least one AEM Component on the page. If there is not, we don’t crawl the DOM.
Line 117: Take the first layer of the DOM (
window.document.body
) and pass it into thecheckChildren
method.
Lines 120 - 127: Next,
init
does the same thing to any shadow DOMs that may exist on the page. (See my article on for details on this architecture.)
Lines 88 - 111:
checkChildren
method. All this method does is check if the current element has children. If so, it decides which method to call on each one.Lines 88 - 90: The
checkChildren
method takes in the current element. Optionally, it can also take in any parent tracking attributes that get passed into it, but those are not a given. For example, on Line 90, it defaults this value to an empty array if none was passed in. And since this first pass is on the top level of the DOM or shadow DOM, there are no parent tracking attributes to pass in yet.Line 93: Generate a list of children one DOM level down — but no deeper than that. (At each layer, we have to perform quite a few potential actions before moving down another layer deeper.)
Line 94: Check to see if at least one child was found. (If not, we skip this entire code block and do nothing — we have reached the end of this portion of the DOM tree.)
Line 95 - 107: If at least one child was found, check each child to see if it is an AEM Component, a button, or an anchor tag.
Line 102: For each item that meets this criteria, invoke the
getTrackingAttributes
method and pass this item into it. If any parent tracking attributes exist, pass those in, too.Line 104: For each item that does not meet the criteria, simply invoke the
checkChildren
method on it recursively without getting or setting any attributes. (Just because this child is not an AEM Component, a button, or a link, does not mean that these types of elements will not be nested beneath it, so we must keep drilling down.)
Lines 54 - 87:
getTrackingAttributes
method. As the name implies, this method only gets the existing tracking attributes on an element. (It does not start merging them with any parent tracking attributes yet — that will be handled by a separate method.)Line 56: An empty array is created to collect any applicable existing tracking attributes.
Lines 58 - 66: First, any
data-track-component-level-X
attribute values that exist are collected in numerical order and pushed into theexistingTrackingAttributes
array. Since we don’t know how many (if any) there will be, a do while loop is used.Lines 68 - 71: Next, if a
data-track-noun
attribute exists, push that value in. (Note that there are use cases where this value may have been set to"no-title"
if the AEM Component was just a container and did not have anything of substance to be included in the click tracking. So we filter those out on Line 69.)Lines 73 - 76: Next, the same actions are performed for
data-track-verb
.Lines 78 - 86: If this is not the first pass that this rule is making on the page (for example, if more AEM Components were added after the page loaded), then a flag will have been set to prevent our code from needlessly refactoring the same attributes on the AEM Components that were already handled in the previous pass. This flag is the
data-track-component-attributes-set="true"
attribute.Line 83: If this attribute is found, we don’t need to do any merging or setting of attributes. Instead, we just invoke the
checkChildren
method on it and pass in the current element plus the existing tracking attributes we just collected, as they may need to get merged in with any child element tracking attributes if necessary.Line 85: If the current element’s attributes have not already been merged, we invoke the
mergeTrackingAttributes
method to handle this. Therefore, we need to pass in both the existing tracking attributes plus any parent tracking attributes so that the two can be merged by that method.
Lines 34 - 53:
mergeTrackingAttributes
method.Lines 35 - 37: Take in the existing tracking attributes, plus any parent tracking attributes, confirm that each are in array form, and then create an empty array where each of their values will be staged in order prior to setting them in the current element’s appropriate tracking attributes.
Lines 39 - 47: Declare the
mergedTrackingAttributesArrPusher
function, which will deduplicate the values as they are merged. Note that this function references theuniqueValue
method for this purpose, which is declared on Lines 27 - 33.Line 49: The parent tracking attributes are deduplicated and staged first, since they should be at the front of the final concatenation.
Line 50: The existing tracking attributes are deduplicated and staged second, since they should be at the end of the final concatenation.
Line 52: All tracking values have now been deduplicated and staged into the array in the order we’ll want them concatenated for Analytics. Which means we’re ready to rewrite the current element’s tracking attributes in this order. This will be done by passing this array into the
setTrackingAttributes
method.
Lines 2 - 26:
setTrackingAttributes
method. This is where the ultimate purpose of this rule is achieved. Now that all parent tracking attributes and existing tracking attributes have been collected, deduplicated, and staged in the order that they should be concatenated for Analytics upon the user’s click, it is time to populate those values into the appropriate data attributes within the current element. To prevent unintended results, all tracking attributes get overwritten.Lines 7 - 9: This code block handles a very specific use case. I discovered times when an AEM Component contained only the
data-track-noun
attribute. This meant that it was the only value to be collected and “merged,” and would be placed indata-track-verb
at this stage — resulting in bothdata-track-noun
anddata-track-verb
having the same value. When clicked, the same value would be concatenated with each other. To prevent this, if only one value is going to be set, we first actively remove thedata-track-noun
attribute from the current element.Lines 11 - 22: Take the array of merged attributes and set them into the appropriate data attributes:
Lines 13 - 15: The last value in the array gets set to
data-track-verb
.Lines 16 - 18: The second-to-last value in the array gets set to
data-track-noun
. (We have to work a bit backward in this manner because we don’t know how many items will be in the array.)Lines 19 - 20: If there are more than two items in the array, now we start from the front and set them into
data-track-component-level-X
(whichever numberX
is for this item — the index number plus 1).
Line 23: Set the
data-track-component-attributes-set
attribute to"true"
. This is the flag that will prevent us from running all this same logic again if the page needs to be crawled more than once.Line 25: And now that we’ve handled this element, it’s time to repeat this entire process on each of its children. So we invoke
checkChildren
once again, and this time we do have merged tracking attributes to pass into it. Conveniently, they are already in themergedTrackingAttributesArr
array, so there is no need to scrape them out of the current element all over again — we just pass that array into thecheckChildren
method.
Once the end is reached and there are no more children to check, the rule stops. All AEM Components are now prepped for our Global | Set Variables | Clicks or Events #25
rule to pull from when the user clicks one. See my Adobe Launch Global Clicks or Events Tracking article for details on how that rule takes it from here.