Launch, Events, and EDDL – aka Jim changed my mind

Sometimes I have trouble with the beginning of an article. This is such a case.

I have been pondering the best way to use Launch to track visitor activity for some time. You know, visitor clicking stuff, SPAs, things like that.

It really boils down to two approaches:

  • Use _satellite.track() and put payload into the calls, or
  • Use a data layer and tell Launch to listen and react to any changes of or additions to the data layer.

You might wonder why I hesitate.

It’s really easy to pass data into Launch using the track() method, yes, but using track() in your code adds a dependency. Using a data layer, on the other hand, allows you to build meaningful Data Elements, and react to data layer changes using one or maybe a couple of Launch Rules.

Until about 4 months ago nope, sorry, I can be precise here: until the 23rd of April, I had thought that listening to a data layer might be more complicated than necessary. You know me: keep it simple!

Then I read Jim Gordon’s excellent appeal for an event-driven data layer, and since then, this article felt like it didn’t need to be written.

Obviously, an EDDL was the best solution! And the computed state thing was beautiful! Loved the concept!

There is even a Launch Extension that implements this, the Data Layer Manager, written by Search Discovery, with good documentation.

So we’re done. Just tell everyone to use this combo, and we’ll all be living happily ever after.

I’m not the only one saying this, btw. Urs Boller agrees, for pretty much the same reasons.

Basics

Unfortunately, I am a nitpicker. I have to do something, on a fairly detailed level, borderline over-the-top, before really feeling that I understand it.

And so here I am, adding an EDDL to my test pages, obviously from scratch.

First things first: what do I need?

  • a “dataLayer” variable, type Array
  • a method to push objects into it
  • some way of generating a computed state
  • some way of knowing that the data layer has changed, and of singaling the change
  • Launch, to listen to and use the data layer

Anything else? No, don’t think so.

For me, personally, items 1, 2, and 5 are the easiest to understand.

The variable is easily created (var dataLayer = []), Javascript Arrays come with a push() method, and I can use the “Custom Event” Event Type provided by the Core Extension in Launch as well as simple Javascript Data Element Type to use the data layer.

I will have to write code that maintains the computed state, ok.

That leaves 1 item open, plus I can see how stuff could be simpler in Launch. The Data Layer Manager Extension cited above provides an Event Type that is much easier to use, so I should think about that, too.

Salt-N-Pepa

For the computed state, and the signalling, I am thinking it would be best to modify or overwrite the push() method.

If we were using Java, I would create a new class “dataLayerArray”, and I would overwrite push() to do more than just add the element. I would also overwrite all other built-in methods from the Array class to no-ops. Don’t want anyone to mess with my data layer!

In Javascript, however, everything is wrong crazy difficult different, and it is only after some pretty surreal conversations with more capable colleagues, that I can now solve make some decisions regarding item 3.

First of all, I am going to write/update computed state on push, i.e. when something is added to the data layer.

The alternative would be to do it on read, and since it would involve iterating, I don’t like that. Call me old-fashioned, but read operations should be O(1), if possible.

So I need to change/amend/overwrite push().

Between the Proxy and Object Composition patterns in Javascript, I opt for a simple Array, amended by a simple function. Simple, and with the added advantage of me being able to understand it.

Here it is:

// helper that copies data into computed state
function cloneObject(target, source) {
    for(var i in source) {
        if(null !== source[i] &&  "object" === typeof source[i]) {
            target[i] = target[i] || {};
            target[i] = cloneObject(target[i], source[i]);
        } else {
            target[i] = source[i];
        }
    }
    return target;
}
// instantiate the data layer
var dataLayer = [];
// add a holder for computed state
dataLayer.computedState = {};
// create a push method that
// - manages computed state
// - sends a custom event
dataLayer.pushAndCompute = function(elem) {
    dataLayer.push(elem); 
    dataLayer.computedState = cloneObject(dataLayer.computedState, elem);
    var event = new CustomEvent('dataLayerChanged', {detail: elem});
    document.dispatchEvent(event);
}

(If you ever build something similar, please do it right!)

Voodoo, simplified

With the above code on my test page, I have covered everything but item 5!

And I have not yet written anything about the format of the data layer! I am thinking that each element pushed onto it should have some structure, of course.

Nor have I spent any thought on how data will get into my data layer, have I?

I’m thinking, naively, that there will be two ways for getting data into the data layer:

  • Simply calling dataLayer.pushAndCompute() with some payload, and
  • an Action Type, provided by the Extension, so Launch can push data into the data layer, too.

I would expect most of the data coming straight from the pages, of course.

In terms of the payload, the format of data, I would keep it simple.

  1. Each element pushed is an object,
  2. Each element pushed must have an event attribute,
  3. Each element can have additional payload

As an example, this is what I push into the data layer when my home page loads:

{
    "event":"Page Load",
    "page":{
        "name":"Home",
        "destinationURL":"https://www.jan-exner.de",
        "destinationURI":"home",
        "author":"Jan Exner",
        "breadCrumbs":["home"],
        "language":"en-GB",
        "geoRegion":"DE",
        "category":"default",
        "pageType":"default"
    },
    "browser":{"type":"desktop"}
}

There is no reason why you’d have to follow my example. Yours could look different.

I guess, for example, that a lot of people will make the “event” item something symbolic, such as “pageLoad”, rather than human readable.

Entirely up to you!

Payload & computed state

The way my implementation of computed state works is that values are added to the overall object, and new elements overwrite existing state.

As an example: on page load, I set page.language to “en-GB”. MY home page then loads more content, of which some might be German. At that point, I add an element to the data layer that sets page.language to “mixed”, and the computed state will reflect the latter at that point.

I have not built that same logic for Arrays (e.g. page.breadCrumbs) so far, but that might or might not have to be done, depending on how you use your data layer.

Think of a category page listing blog articles. I could add all articles to the data layer in one fell swoop, like so:

dataLayer.pushAndCompute({
    event: 'articleList',
    articles: [
        {id: 'b190912', name: 'blabla1'},
        {id: 'b190801', name: 'blabla2'},
        {id: 'b190724', name: 'blabla3'}
    ]
});

Or I could add each article individually, like so:

dataLayer.pushAndCompute({
    event: 'addArticleListing',
    article: {
        id: 'b190912',
        name: 'blabla1'
    }
})

In the latter case, I would have to make sure the computed state adds articles up rather than overwriting each time I add a new article.

Again, both valid approaches, entirely up to you.

What I do not like about case two is that the behaviour of the pushAndCompute() method would depend on the event. I should maybe think about an optional parameter that tells the data layer to add. Might be simpler.

The simplest way, for now, is to just write the whole array, each time it changes. Keep it simple.

Always more

If you want to do this right, define your data layer, and create a JSON Schema for the elements and computed state. That way, you can create a cloneObject() method that always does the right thing.

And while we’re talking about doing it right: if you use an EDDL in a single-page application, your data layer will at some point grow beyond reasonable.

You should build it so the pushAndCompute() method removes entries from the beginning of the data layer, maybe limiting it to 100 elements, or removing things that don’t belong to the current “page”.

In essence, you need a garbage collector for your data layer.

On top of that, I wonder whether the pushAndCompute() method shouldn’t queue any new data, then asynchronously work on that queue.

The drawback is that when you read from the data layer, you might not always have all the latest data. But the advantage is that you will never run into race conditions, overlapping modifications, or un- or misdefined states.

Always more after that

One really big question is: can the data layer support code live in Launch? Can you build it into the Extension?

My feeling is that this would not work.

The dataLayer array and pushAndCompute() method have to be available early in the page, ideally before Launch is loaded.

That is even more a necessity if Launch is loaded async. And I think it should be loaded async.

A more complex approach would be for the data layer to signal when it is ready, and Launch could listen to that. Overkill, at least for me, at least for now.

So a library file it is, defining the actual data layer as well as all supporting methods.

The Extension would be a “companion”, built to make it easy to work with the data layer in Launch.

To me that sounds like a good team.

Now I only have to build me an Extension…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.