Adobe Launch Global Clicks or Events Tracking
Overview
At my company, there is a core set of tracking events and variables we send to Adobe Analytics for all events.
For the purposes of this explanation, let’s say I work at a solar power company. (All variables and attributes in this article have been changed to suit that scenario.) To keep things D.R.Y., this core set of variables is programmed into one Adobe Launch rule named Global | Set Variables | Clicks or Events #25
, which fires on every event we want to track.
In addition to those baseline variables, if that event is a click, we also send:
event1: Custom Link Click (increment)
eVar1: Custom Link Value (describes which element was clicked)
Basic Custom Link Values
The Custom Link Value that gets passed into eVar1 is typically a concatenation of two data attributes, which I put into each element we want to track clicks on:
data-track-noun
(represents “what” is being clicked)data-track-verb
(represents the “action” the user is taking)
These two values get concatenated, colon-delimited, and assigned to eVar1. For example, if my site had a “Careers” card containing a “Learn More” button, that button would contain:
data-track-noun="careers" data-track-verb="learn-more"
(We standardize these values to lowercase kebab-case.) The resulting value passed in eVar1 would be careers:learn-more
.
Accordion ‘Open’ vs. ‘Close’
Sometimes it gets more complex than this. Maybe there is an FAQ page containing a list of questions. Each question is an accordion that can be opened or closed by clicking on it, and our analysts want to know which is occurring.
In this case, I would append a value of ‘open’ or ‘close’ to eVar1:
faq:who-installs-your-panels:open
faq:who-installs-your-panels:close
This, too, requires custom logic to only append such a value if the situation calls for it.
Nested AEM Components
Even more complex is if the page has AEM components nested inside each other in any order and any combination you might imagine. (See my article on Adobe Launch Nested AEM Component Click Tracking for details.)
If this is the case, we will have additional attributes such as data-track-component-level-X
(with X
being a number) to also concatenate prior to the data-track-noun
and data-track-verb
values:
data-track-component-level-1="employment"
data-track-noun="research-development"
data-track-verb="learn-more"
Giving us results like: employment:research-development:learn-more
.
Shadow DOM Clicks
All of the above needs to work the same way if a user clicks an element inside a shadow DOM. (For example, our global header and footer are each inside a shadow DOM.)
As of this writing, Adobe Launch does not detect clicks in shadow DOMs, so I have to simulate those by passing the clicked element into this rule via direct call object. (See my article on Adobe Launch Shadow DOM Click Tracking for how this is accomplished.)
So, this Global | Set Variables | Clicks or Events #25
rule needs to be able to take in that object, treat it as if it were a regular clicked element, and then account for all the same use cases listed above.
Direct Call Objects
There are some additional use cases for things like adding items to a cart, or updating their quantities, which are handled via direct call. These direct calls often pass an object containing only the custom data that is needed. So in these cases, this rule needs to know how to parse that custom data and standardize it into the Custom Link Value we would normally pass.
Sending and Clearing the Variables
One last architectural note before I move on to the syntax of the custom code itself. This Global | Set Variables | Clicks or Events #25
rule sets the variables, but it does not send and clear them. That function is handled by a separate rule called Global | Send, Clear Variables | Clicks or Events #100
rule.
The #25
and the #100
at the end of each rule’s name represents the “order” each one is programmed to fire in. This ensures that the #25
rule fires first, and the #100
rule fires second. This also allows us to fire additional custom click tracking rules in between order #25
and order #100
so that we can roll some additional data into certain clicks if we want to and include it in the same network call.
This means that both rules need to trigger on all the exact same events in order to stay in sync — with one difference:
Both rules listen for clicks on anchor tags.
Both rules listen for clicks on buttons.
The “Set Variables #25” listens for the
set-global-click-or-event-variables
direct call.The “Send, Clear Variables #100” listens for the
send-global-click-or-event-beacon
direct call.
Syntax for Custom Link Values
For context, here is the full custom code block. (Scroll past it for a step-by-step explanation of each part.)
let ruleTriggerType = event.$type.replace('core.', '');
let clickedElement = '';
let clickedElementAttributes = '';
let clickAttributes = {};
let accordionStatus = '';
const customLinkValues = [];
// The following are used if data-track-verb is missing:
let childNodes = '';
let className = '';
let imgTag = '';
let name = '';
let parentNode = '';
// Helper Functions
function cleanText(_txt) {
if (!_txt) return;
let clean = _txt.trim();
if (!clean) return;
let removeStr;
const startPos = (clean.indexOf('') || clean.indexOf(''));
const endPos = (clean.indexOf('') || clean.indexOf('')) - startPos + 6;
if (startPos > -1) {
removeStr = clean.substr(startPos, endPos);
clean = clean.split(removeStr).join('');
}
clean = clean.replace(/[^a-z\d\s]+/gi, ' ');
clean = clean.trim();
clean = clean.replace(/\s{2,}/g, ' ');
clean = clean.split(' ').join('-').toLowerCase();
var whSpace = /\s/;
clean = clean.replace(whSpace, '-');
return clean;
}
const getLinkName = (_element) => {
const navName = _element.getAttribute('data-nav-name') || '';
const navText = _element.getAttribute('data-us-text') || '';
const navTitle = _element.getAttribute('title') || '';
const navTitle2 = _element.getAttribute('data-us-title') || '';
const altTag = _element.getAttribute('alt') || '';
let elementText = '';
if (_element.hasAttribute('aria-label')) {
elementText = _element.getAttribute('aria-label');
} else {
elementText = _element.textContent;
}
if (altTag === '' || !elementText) {
if (!elementText) {
elementText = $(_element).clone().children().remove().end().text() || '';
if (_element.nextElementSibling) {
if (_element.nextElementSibling.firstElementChild) {
elementText = _element.nextElementSibling.firstElementChild.textContent || '';
}
}
}
}
let linkName = elementText || navName || navText || altTag || navTitle || navTitle2 || '';
return linkName;
}
// Initial Standardization for Clicks, Direct Calls, and Shadow DOM Clicks:
switch (ruleTriggerType) {
case 'click':
case 'change':
clickedElement = this;
s.events = s.apl(s.events, 'event1', ',');
break;
case 'direct-call':
if (!!event && !!event.detail) {
if (!!event.detail.clickedElement) {
clickedElement = event.detail.clickedElement;
}
if (!!event.detail.shadowClick) {
ruleTriggerType = 'shadowClick';
s.events = s.apl(s.events, 'event1', ',');
}
}
break;
}
if (!!clickedElement && !!clickedElement.attributes) {
clickedElementAttributes = [...clickedElement.attributes];
}
if (!!clickedElementAttributes) {
clickedElementAttributes.forEach(clickedElementAttribute => {
const key = clickedElementAttribute.name;
const value = clickedElementAttribute.value;
clickAttributes[key] = value;
});
}
// Post-Standardization Logic:
switch (ruleTriggerType) {
case 'click':
case 'shadowClick':
if (!clickAttributes['data-track-verb']) {
// If data-track-verb is missing:
name = 'na';
className = (clickAttributes.class) ? clickAttributes.class : '';
childNodes = clickedElement.childNodes;
parentNode = clickedElement.parentNode;
imgTag = clickedElement.querySelector('img');
if (imgTag) name = getLinkName(imgTag) || name;
if (name === 'na') name = getLinkName(clickedElement) || name;
if (
(
!!className &&
typeof className === 'string'
) &&
parentNode.parentNode.dataset.track === 'accordion-section'
) {
name = parentNode.parentNode.getAttribute('data-track-verb');
accordionStatus = clickedElement.getAttribute('aria-expanded') === 'true' ? 'close' : 'open';
}
clickAttributes['data-track-verb'] = name;
}
break;
case 'direct-call':
if (!!event && !!event.detail && !event.detail.clickedElement) {
if (!!event.detail.trackNoun) {
clickAttributes['data-track-noun'] = event.detail.trackNoun;
}
if (!!event.detail.trackVerb) {
clickAttributes['data-track-verb'] = event.detail.trackVerb;
}
}
break;
}
// Final cleanup before concatenation occurs:
for (const property in clickAttributes) {
if (property.includes('data-track')) {
clickAttributes[property] = cleanText(clickAttributes[property]);
}
}
// Stage the custom link values in the order they will be concatenated:
let level = 1;
let dataTrackComponentLevelX = '';
do {
dataTrackComponentLevelX = clickAttributes[`data-track-component-level-${level}`];
if (!!dataTrackComponentLevelX) {
customLinkValues.push(dataTrackComponentLevelX);
}
level++;
} while (!!clickAttributes[`data-track-component-level-${level}`]);
if (!!clickAttributes['data-track-noun']) {
customLinkValues.push(clickAttributes['data-track-noun']);
}
if (!!clickAttributes['data-track-verb']) {
customLinkValues.push(clickAttributes['data-track-verb']);
}
if (!!accordionStatus) {
customLinkValues.push(accordionStatus); // 'open' vs. 'close'
}
// Then cap each one at 60 characters so we don't go over Adobe's 255-character limit when concatenated:
const cappedLinkValues = customLinkValues.map(customLinkValue => {
if (customLinkValue.length > 60) {
customLinkValue = customLinkValue.slice(0, 60);
}
return customLinkValue;
});
// Then join the capped link values with colons:
const customLinkValuesConcat = cappedLinkValues.join(':');
s.eVar1 = customLinkValuesConcat;
s.linkTrackVars = s.apl(s.linkTrackVars, 'eVar1,events', ',');
s.linkTrackEvents = s.events;
Explanation
Line 1: I want to know which type of event triggered the rule. The event could be a click, a shadow DOM click, a direct call, a ‘change’, etc. This will be used as a starting point to standardize the data coming in. I also strip out the
core.
that is always going to be present. (Note thatlet
is used instead ofconst
because there will be some manipulation of this value if the event was a shadow DOM click. More on that later.)Lines 2 - 6: I declare a set of empty variables where certain data will be stored, if certain conditions are later met:
Line 2: For the clicked element (regular or simulated) itself.
Line 3: Will become an array where all the attributes of the clicked element will be stored.
Line 4: For the key/value pairs for each attribute after they are converted from the above array.
Line 5: For the ‘open’ or ‘close’ value on an accordion click.
Line 6: At the end, I will push just the values the anaylists want to track, in the order they want to track them, into this array.
Lines 9 - 13: Declare variables that will be used if the
data-track-verb
attribute is missing from the clicked element.Lines 16 - 38:
cleanText
is a helper function that standardizes text to lowercase kebab-case.Lines 40 - 67:
getLinkName
is a helper function that contains fallbacks for getting a click tracking value from other places in the element if the normaldata-track
attributes are missing.Lines 70 - 88: Next, I determine what I want to consider the clicked element to be:
Line 73: If the rule was triggered by a ‘click’ or a ‘change’, the clicked element will be the ‘this’ keyword.
Line 74: Since I know this use case means that it should be tracked as a click, I go ahead and increment
event1
now.Lines 78 - 80: If the rule was triggered by a direct call, it may have received an object along with it that I want to treat as the clicked element.
Line 82: If the rule was triggered by a shadow DOM click, I reassign its
ruleTriggerType
value toshadowClick
so that I can differentiate between the two in my logic later.Line 83: And if this event was a shadow DOM click, I can also go ahead and increment
event1
now.
Lines 89 - 91: Now that I’ve determined what I want to use as my clicked element, I can pull the attributes from it and store them in an array. (The validation is necessary because
clickedElement.attributes
will not work if a direct call passed a custom object into this rule containing only the specific data that is needed. That use case will be accounted for later.)Lines 93 - 99: Convert the array of attributes into key/value pairs and store them in the
clickAttributes
object. (This is the format that will be used for the rest of the rule.) Once again, validation is necessary in case the object passed into this rule does not contain architecture consistent with that of a clicked element.Lines 103 - 114: If the event was triggered by either a regular click or a shadow DOM click, and if the clicked element is missing the
data-track-verb
attribute, fallback logic is used to source a value from other attributes instead.Lines 116 - 125: If the clicked element is an accordion, the accordion status is set to either ‘open’ or ‘close’ based on the
aria-expanded
attribute.Line 126: In the end, I store the fallback value in the
clickAttributes
object’sdata-track-verb
property.Lines 129 - 138: If the rule was triggered by a direct call with a custom object passed into it, I look specifically for those custom properties here.
Lines 142 - 147: Finally, the
clickAttributes
object contains all the key/value pairs it’s going to get. The properties that will ultimately be passed to Adobe Analytics start withdata-track
. Those are the only ones I need to convert to lowercase kebab-case. (And I do need to do this in case some of these values were indeed pulled from fallbacks such as link text, alt tags, aria-labels, etc. They could include special characters, spaces, capital letters, and more.)Lines 149 - 169: Now that all of the tracking values have been standardized to lowercase kebab-case, it is time to stage them in the order that they will be concatenated and passed to Adobe Analytics in eVar1. This is done by simply pushing them into an array in the desired order:
Lines 149 - 157: That means I stage all the
data-track-component-level-1
,2
,3
, etc. values first, and in numerical order. But I have no way of knowing how many there will be, or if there will even be any at all. (This use case will only apply if there are AEM components on the page. See my article on Adobe Launch Nested AEM Component Click Tracking for full details.) Therefore, I use ado while
loop.Lines 159 - 161: If there is a
data-track-noun
value, I stage that next.Lines 163 - 165: If there is a
data-track-verb
value, I stage that next.Lines 167 - 169: Lastly, if this was an accordion click, I stage either
open
orclose
.
Lines 172 - 177: Adobe has a 255-character limit, and my concatenation could potentially get pretty long. (Especially if there are a lot of layers of nested components, or if the text from an anchor tag was used as a fallback.) If the custom link tracking value goes over 255 characters, Adobe will truncate it. I'd rather control how that truncation occurs, so I cap each individual value at 60 characters. (The ballpark estimation is that our site will likely not have more than four layers of nested components — plus perhaps an ‘open’ or ‘close’ appendage at the end if this was an accordion click.)
Line 180: Finally, the actual concatenation is done by joining the items in the array with colons.
Lines 182 - 184: Assign all the values to their respective places in Adobe’s
s
object.