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:

  1. When the page loads, I crawl it and collect any shadow DOMs that exist.

  2. For each shadow DOM that is found, I attach a ‘click’ event listener to its shadowRoot.

  3. 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.

  4. 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

  1. document.getElementsByTagName('*') gets all tags on the page.

  2. However, that returns an HTMLCollection. So I wrapped it inside a spread operator [...] to convert it to an array.

  3. Now that I had an array, I could call the .filter method on it.

  4. .filter(tag => !!tag.shadowRoot) checks each tag in the array. If a tag contains a shadowRoot, then that tag represents a shadow DOM, and the filter will include it. (If not, the filter will ignore that tag.)

  5. 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 called shadowDOMs.

  • 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 our Global | 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 (the e parameter this function takes in). Therefore, the first thing I did was add some validation to make sure that e.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 your Global | 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 one parentNode.

  • 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 the clickData 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 entire clickData 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 the shadow 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 this shadowRoot is detected, all it has to do is invoke the shadowClicks 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.