Adobe Launch Shadow DOM Click Tracking
Overview
When the company I was working for introduced a new global header and footer that existed in the shadow DOM, I faced a new challenge:
It turned out that Adobe Launch did not automatically detect clicks inside of shadow DOMs. We already had a global rule in Launch to track clicks and events. (Called Global | Set Variables | Clicks or Events #25
— see my article on Adobe Launch Global Clicks or Events Tracking for details on how it works.)
But it wasn’t getting triggered. Therefore, I had to come up with a custom code workaround to detect shadow DOM clicks manually.
The overall architecture I came up with is:
When the page loads, I crawl it and collect any shadow DOMs that exist.
For each shadow DOM that is found, I attach a ‘click’ event listener to its shadowRoot.
If a click is detected by this event listener, my code determines what the clicked element was and standardizes it to the type of object that the
Global | Set Variables | Clicks or Events #25
rule (for regular DOM clicks) expects.Then I manually trigger the
Global | Set Variables | Clicks or Events #25
rule, and send it the standardized object that was created by my custom code.
The custom code is explained in detail below.
Shadow DOM Data Element (‘shadow doms’)
I figured that I might want the ability to crawl the page for shadow DOMs in other applications, too. So rather than putting the custom code directly in my Shadow DOM rule in Adobe Launch, I put it in a data element that could be referenced from anywhere.
I named this data element shadow doms
, and it only contains one line of code:
return [...document.getElementsByTagName('*')].filter(tag => !!tag.shadowRoot);
Explanation
document.getElementsByTagName('*')
gets all tags on the page.However, that returns an HTMLCollection. So I wrapped it inside a spread operator
[...]
to convert it to an array.Now that I had an array, I could call the
.filter
method on it..filter(tag => !!tag.shadowRoot)
checks each tag in the array. If a tag contains ashadowRoot
, then that tag represents a shadow DOM, and the filter will include it. (If not, the filter will ignore that tag.)Last but not least, I
return
all of this, or else my data element will do all this work but not spit out the results at the end.
Now, any time I run _satellite.getVar('shadow doms')
, I receive an array containing all the shadow DOM tags that were filtered out of the entire page. (If none were found, it will return an empty array.)
Shadow DOM “Add Event Listener” Rule
Once I had my shadow doms
data element working, I was ready to use it in an Adobe Launch rule. I created a rule containing custom code that, if it finds any shadow DOMs on the page, attaches a ‘click’ listener to each one. I named it Global | Add Shadow DOM Event Listeners
.
First, the full code block for context. (Scroll past it for a breakdown of what each part does.)
const shadowDOMs = _satellite.getVar('shadow doms');
const shadowClicks = (e) => {
const clickData = {
shadowClick: true,
clickedElement: '',
};
if (!!e.target) {
if (e.target instanceof SVGElement) {
if (!!e.target.getAttribute('d')) {
// Path elements *inside* SVG tags:
clickData.clickedElement = e.target.parentNode.parentNode;
} else {
// SVG tags themselves:
clickData.clickedElement = e.target.parentNode;
}
} else {
// All other clicks (non-SVG):
clickData.clickedElement = e.target;
}
}
if (!!clickData.clickedElement) {
_satellite.track('set-global-click-or-event-variables', clickData);
_satellite.track('send-global-click-or-event-beacon');
}
}
if (!!shadowDOMs && Array.isArray(shadowDOMs) && shadowDOMs.length > 0) {
shadowDOMs.forEach(shadowDOM => {
shadowDOM.shadowRoot.addEventListener('click', shadowClicks);
});
}
Explanation
First, a quick note: I programmed the rule itself to be triggered on every page/route load.
Line 1: The first thing the custom code inside the rule does is grab the
shadow doms
data element and store it in a variable calledshadowDOMs
.Line 3: Next, I created a function called
shadowClicks
that gets triggered whenever a ‘click’ event is detected in a shadow DOM. It takes in the parameter(e)
for the click event.Line 4: The first thing this function does is create an object called
clickData
. This object will later be passed to ourGlobal | Set Variables | Clicks or Events #25
rule. It simulates the clicked element as it would have occurred in the regular DOM.Line 5: The
clickData
object contains the property:shadowClick: true
(Note that
true
is a Boolean, not a string.)This flag is important when this object is later passed to the
Global | Set Variables | Clicks or Events #25
rule. It allows me to differentiate between regular DOM clicks and shadow DOM clicks. But for now, let’s not go down this rabbit hole.Line 6: The next property that the
clickData
object contains is:clickedElement: ''
At the beginning, it is defaulted it to an empty string. But after the rest of the logic in this function determines what exactly should be considered the clicked element, that element will be stored here.
Line 9: Next is the logic that determines which element in the shadow DOM got clicked. This typically involves the
target
in the click event (thee
parameter this function takes in). Therefore, the first thing I did was add some validation to make sure thate.target
even exists:if (!!e.target) {}
Lines 10 - 17: I found out the hard way that if the clicked element is an SVG, the
clickData
object that you pass to yourGlobal | Set Variables | Clicks or Events #25
rule is going to be a mess. So I accounted for this specifically as follows:Lines 11 - 13: The user may have clicked on the SVG tag itself, or they may have clicked on the path elements inside the SVG tag. This first code block handles clicks on path elements.
Note that this condition must be nested inside the
instanceof SVGElement
condition, as that will also be true.This code block looks two parentNodes up from the path element, then stores the result in the
clickData
object.Lines 14 - 16: The
else
condition accounts for clicks directly on the SVG tags. For this use case, we only have to go up oneparentNode
.
Line 18: That’s it for SVGs. I now exit the SVG condition(s) and account for all other (non-SVG) clicks. This use case is much simpler — it just grabs
e.target
and stores it in theclickData
object.Line 24: Now the clicked element (if it exists) has been selected and my code is ready to pass it to the
Global | Set Variables | Clicks or Events #25
rule. Once again, for safety, I start with some validation to confirm that a clicked element was found before I go so far as to send it:if (!!clickData.clickedElement) {}
Line 25: Inside this condition, I first trigger the
Global | Set Variables | Clicks or Events #25
rule that sets variables, and then I pass the entireclickData
object into it.The
Global | Set Variables | Clicks or Events #25
rule will take it from here. (See my article on Adobe Launch Global Clicks or Events Tracking for how that works.)Line 26: Immediately after that, I trigger the
Global | Send, Clear Variables | Clicks or Events #100
rule that sends the beacon and clears the variables.(Once again, see my article on Adobe Launch Global Clicks or Events Tracking for an explanation on this architecture.)
Lines 30 - 34: That’s it for the function. But it still needed an event listener to trigger it. The listener is wrapped in some validation to make sure that
shadowDOMs
exists, that it is an array, and that this array contains at least one item. (Remember theshadow doms
data element I stored in a variable back on line 1? This is where that variable starts getting used.)This validation is important because if no shadow DOMs exist on the page . . . I don’t want the rest of this code block to execute and start throwing errors.
Line 31: Once I’m certain that
shadowDOMs
is an array containing at least one item, I am safe to run the.forEach
method on it.Line 32: ‘For each’ shadow DOM in the array, I attach a ‘click’ listener to its
shadowRoot
. And when a click on thisshadowRoot
is detected, all it has to do is invoke theshadowClicks
function.
Summary
Every time a page/route loads, the Global | Add Shadow DOM Event Listeners
rule fires. It runs the shadow doms
data element. If that data element returns at least one shadow DOM, the rule sets up a click listener on each of those shadow DOMs. Then, when a shadow DOM click is detected, it will capture the data from the clicked element and pass it over to the Global | Set Variables | Clicks or Events #25
rule, simulating a regular-DOM click.
See my Adobe Launch Global Clicks or Events Tracking article for details on how the Global | Set Variables | Clicks or Events #25
rule handles the rest.