Tracking Events in Web Components / Shadow DOM

  • 22 June 2022
  • 0 replies
  • 137 views
Tracking Events in Web Components / Shadow DOM
Userlevel 4
Badge +3

Over the past 6 months, I’ve seen a sharp increase in the number of customers building their web apps using something called the Shadow DOM. Sounds spooky in a Stranger Things kinda way, but it’s a fairly simple idea. As analytics guru Simo Hava says in his excellent article on tracking interactions in the shadow dom (from which this post borrows liberally—I recommend reading it for a fuller understanding of what’s going on here):

 

The shadow DOM is a way to add HTML structures to the web page so that they remain isolated from the rest of the document object model (DOM). It’s a popular concept with web components, as it allows for encapsulation of web structures so that they aren’t affected by style declarations of the parent tree, for example.

 

Unfortunately, being isolated from the regular DOM means these events are also currently hidden from Heap’s autocapture.

Happily, Simo offers a way to track events; I’ve modified his ideas, and added some of my own, to help you track these click events in Heap, pseudo-autocapture style. 

This set up will require some help from your engineering teams, but far, far less than it would require for them to manually tag every action individually. 

 

A Tale of Two Functions

 

There are two parts to setting up this tracking. First, a modified version of the function in Simo’s article. Basically, it adds a click listener for events bubbling from the shadow DOM, captures some key information about that click, and sends it to Heap. This example captures a hierarchical representation of the DOM, the text of the clicked element, and the link of the clicked element. It’s the “same” as the builtin Hierarchy property, but will be used slightly differently, as you’ll see later. 

(function() {
// Set to the event you want to track
var eventName = 'click',
useCapture = true,
trackOnlyShadowDom = true;

var callback = function(event) {
if ('composed' in event && typeof event.composedPath === 'function') {
// Get the path of elements the event climbed through, e.g.
// [span, div, div, section, body]
var path = event.composedPath();

// Fetch reference to the element that was actually clicked
var targetElement = path[0];
// Create the Heap-style hierarchy
var hierarchy = getHierarchy(targetElement);
// Check if the element is WITHIN the shadow DOM (ignoring the root)
var shadowFound = path.length ? path.filter(function(i) {
return !targetElement.shadowRoot && !!i.shadowRoot;
}).length > 0 : false;

// If only shadow DOM events should be tracked and the element is not within one, return
if (trackOnlyShadowDom && !shadowFound) return;

// Send to Heap
// Any property from the event.target may be added e.g. targetElement.ariaLabel
event_name = 'shadowdom_event_' + event.type;
properties = {
shadowHierarchy: hierarchy,
shadowTargetText: targetElement.innerText || targetElement.textContent || null,
shadowHref: targetElement.href || targetElement.action || null,
inShadowDom: shadowFound
};
heap.track(event_name, properties);

}
};

document.addEventListener(eventName, callback, useCapture);
})();

The second part is the function which creates the Heap-style hierarchy. 

function getHierarchy(target) {
var tags = [];
var classes, hierarchy, fullHierarchy, attributes, attributesBlacklist;
while (target && target.tagName != 'BODY') {
hierarchy = '@' + target.tagName.toLowerCase() + ';';

if (target.id) {
hierarchy += '#' + target.id + ';';
}
classes = Array.from(target.classList);

if (classes.length > 0) {
hierarchy += '.' + classes.join(';.') + ';';
}

attributesBlacklist = ["class", "id", "password", "style", "ng-", "react-id", "value"];

attributes = target.getAttributeNames().filter(name => !attributesBlacklist.some(substring => name.includes(substring)));

for (i = 0; i < attributes.length; i++) {
hierarchy += '[' + attributes[i] + '="' + target.getAttribute(attributes[i]) + '"];';
}

tags.unshift(hierarchy);
if (target.parentElement) {
target = target.parentElement;
} else {
target = target.getRootNode().host;
}
}

fullHierarchy = tags.join('|');

if (fullHierarchy.length <= 1024) {
return fullHierarchy;
} else {
return fullHierarchy.slice(-1024).split('|').slice(1).join('|')
}
}

If you’re feeling adventurous, you can paste both these functions into your browser’s dev console right now, click on your shadow DOM elements, and see events named shadowdom_event_click show up in Live View…. Neat!!

To deploy for real, just ask your engineer to add these functions to a js file and source it when they load the page along with other javascript.

 

Defining Events

 

Because these events are sent to Heap via our API, you won’t be able to define these events in the Event Visualizer but in the Definitions tab of Heap. Doing so is not hard, but you’ll benefit from having a little bit of knowledge about HTML and CSS. (Heap University has some great resources on this.)

You can use this event just like any custom event in Heap. Go to Definitions > New Definition > New Event > Custom, pick shadowdom_event_click from the dropdown, and use the filters to specify the event. You can use the shadowTargetText and shadowHref properties as you would with any Heap event that captured text or links. Or you can use the shadowHierarchy, and filter based on substring matches (contains or wildcard matches). This will make a bit more sense if you QA them by looking at the Live View first as you click around. As long as your markup is decent, it should be easy enough to figure out what’s what.

Using shadowTargetText to define an event
Using shadowHierarchy with a contains filter to define an event.

Full value looks like @div;.row-show-icons;|@div;.link-wrapper;|@button;.link;.animated;[data-modal-trigger=""];[role="button"];[aria-label="Send via Email"];[data-key="EMAIL"];[data-ol-has-click-handler=""];|@span;

 

I hope this helps unblock your event tracking in the Shadow DOM! Please let me know in the comments if you have any questions!


0 replies

Be the first to reply!

Reply