Lean Analytics with the Adobe Client Data Layer

This is a guest post by my colleague Ben Wedenik, one of the people who wrote the Adobe Client Data Layer Extension for Launch.

When my colleagues and I started to develop the “Adobe Client Data Layer” Launch Extension, we wanted to make the concept and technology of an Event Driven Data Layer (EDDL) available to anyone. Just recently, we released version 1.1.3 of the Extension and underlaying data layer.

In this article I want to give you some best practices about how the extension can be actually used in an elegant and scalable way to get your tracking setup to the next level.

As Jan has already written a nice post about what the Adobe Client Data Layer (ACDL) is, I’m for now assuming that you’re familiar with the concept behind it. If not, I’d recommend you to read his article first

Let’s put together a wish list of features we would like to cover:

  • Tracking of arbitrary events.
    • Pageload
    • Downloads
    • Social shares
    • Navigations
    • Video interactions
    • Clicks
    • Etc.
  • “Global” variables which are set on every hit.
    • Pagename
    • Language
    • Timestamp
    • Etc.
  • Ability to set specific variables for specific events.
    • E.g. CTA label on a CTA click event.
  • Ability to set specific Adobe Analytics events for specific events.
    • E.g. Set event123 if a CTA click is registered.
  • Low complexity and easy visual editing.

If you’re a huge fan of having tons of rules and data elements, you should probably stop reading now because the setup I’m going to propose will be lean (really lean).

Two rules and one data element – that’s all it needs to cover the items of our wish list.

Let’s have a closer look of what the respective components are going to do:

  • Clear and Set Variables — This Rule is going to be setting all the variables we need.
  • Fire Beacon — This Rule fires the Adobe Analytics beacon.
  • Event Mapping Table — In this Data Element we will define our events and some additional variables if needed.

To make it easier to follow and less abstract, I’m going to illustrate my setup based on a fictive scenario containing three events with different requirements. This can be scaled up to fit many more events, while the concepts stay the same.

Those are the given requirements in our case:

  • Pageload
    • event1
    • eVar1 = Pagename
    • eVar2 = Language
    • eVar3 = Page Category
    • Those eVars should be set on every hit = global variables
  • CTA Click
    • event2
    • eVar4 = CTA Label
  • App Download
    • event3
    • eVar5 = iOS or Android

    Rule 1: [10] Clear and Set Variables

    [screenshot]
    Rule 1 – Clear and Set Variables
    The Event is basically a push listener to all events with an execution order of 10, which is defined to make sure, this Rule is fired before the following fire beacon Rule.
    [screenshot]
    Rule 1 – Data Layer Push Event Type Settings
    Alternatively – if your website is firing events you don’t want to track – one can simply add multiple listeners to specific events:
    [screenshot]
    Rule 1 – Clear and Set but with specific events
    So far so good, this Rule should now fire whenever one event (or the respective events) is pushed into the ACDL. Let’s go ahead and define the desired variables in the action.
    [screenshot]
    Set eVars
    In this rule we can set all the required eVars. As you can see, eVar1-3 are populated by referring to the “fullState” of the ACDL. This is basically the value of the getState() function at the time the event is captured. By doing so, we are sure that the page name, language and page category are always going to be set even though this information might not be directly set on the data layer event.eVar4 and eVar5 get their values directly from the “eventInfo”, which is the part of the ACDL which is not persisted into the state.But why does this setup work? Well, if there is no value in the respective eventInfo object, the eVar will remain empty and thus will not be included on the server call. Thus, the pageload will only contain eVar1-3 while, for example, the CTA Click will be fired with eVar1-4.We’ll update this rule in the end again to support setting our events based on the mapping table.

    Rule 2: [90] Fire Beacon

    In this rule the Adobe Analytics beacon is going to be fired. Based on the respective event, this should result in a pageview or not.

    [screenshot]
    Rule 2 – Fire Beacon
    The defined Launch event trigger should be the same as in the previous rule. This means either “All Events” or your specific list of events, now with a higher order, in my example I’m using 90 to make sure the rule is fired after clear and set variables is executed. To avoid the need of having two separate rules to either fire a pageview or a custom link, I’m using little piece of JavaScript which does the magic for us.
    The custom code needs to be added in the Custom Code section of the “Adobe Analytics – Set Variables” action, as we need a reference to the s object to call s.t() or s.tl().
    [screenshot]
    Analytics Custom Code

    s.useBeacon = true;
    if (event.message.event === 'pageload') {
        s.t();
    } else {
        s.tl(true, 'o', event.message.event);
    }
    

    In case of a pageload event, a pageview is triggered, else a custom link with the event name is issued.

    Data Element: eventMappingTable

    In the mapping table we can now define which event should be set on each ACDL event.

    [screenshot]
    Event Mapping Table
    The data element we base our mapping table on is not a “real” data element in fact. I’m simply referring to the event name (of the pushed ACDL event) available once this mapping table will be evaluated. The output is the respective event.

    Gluing the mapping table with the set variables rule

    As a last step the integration of the mapping table into the clear and set variables Rule is needed.

    This requires some custom code, which also needs to be added in the Custom Code section of the “Adobe Analytics – Set Variables” action, as we need the reference to the s object to set additional events.

    The following code will call the mapping table, pass through the ACDL event and then set the respective event based on the result:

    /**
     * Sets AA events based on an input string
     * @param mappedEvent name of the event to set, e.g. 'event1'
     */
    function setAnalyticsEvent(mappedEvent) {
        if (typeof mappedEvent !== 'undefined' && mappedEvent !== null && mappedEvent !== '') {
            // Set mappedEvent into s.events
            if (typeof s.events === 'undefined') {
                s.events = '';
            } else if (s.events !== '') {
                s.events += ','
            }
            s.events += mappedEvent;
    
            // Update s.linkTrackEvents to contain mappedEvent
            if (typeof s.linkTrackEvents === 'undefined') {
                s.linkTrackEvents = '';
            } else if (s.linkTrackEvents !== '') {
                s.linkTrackEvents += ',';
            }
            s.linkTrackEvents += mappedEvent;
    
            // Add events to s.linkTrackVars if not already present
            if (typeof s.linkTrackVars === 'undefined' || s.linkTrackVars === null) {
                s.linkTrackVars = '';
            }
            if (s.linkTrackVars.indexOf('events') === -1) {
                if (s.linkTrackVars !== '') {
                    s.linkTrackVars += ',';
                }
                s.linkTrackVars += 'events';
            }
        }
    }
    
    // Retrieve the event to set from the mapping table, passing in the current event.
    setAnalyticsEvent(_satellite.getVar('eventMappingTable', event));
    

    Congratulations! You’re done!

    With this setup it is straight forward to add further events and eVars. Of course, the default variables like pagename can be set in the Clear and Set Variables rule too. Looking at our requirements, we can tick off all the points from the wishlist:

    • Tracking of arbitrary events
      • All events pushed into the ACDL will be available in Launch.
    • “Global” variables which are set on every hit
      • Using the fullState of the ACDL Extension this is easy to set up.
    • Ability to set specific variables for specific events
      • By leveraging the mapping table extension, this can be achieved flexibly.
    • Ability to set specific Adobe Analytics events for specific events.
      • By using the event information directly this can be done within one rule.
    • Low complexity and easy visual editing.
      • Using the given Adobe Analytics Set Variables action we can work visually.
      • Setting events based on the mapping table is intuitive and readable without the need of understanding JavaScript.

    Pro Tip

    With a few more lines of JavaScript you can boost your mapping table to support setting multiple events at once. Simply return ‘event1,event2,event3’ from the mapping table and iterate through the list in your code.

    If you are eager, you can even go one step further by adding support for structures like this:

    ‘event1,event2=456,eVar7=%event.message.eventInfo.myAttribute%,eVar9=%anotherMappingTable%’

    I’ve implemented this successfully in real world projects and the usage is absolutely flexible, as the mapping table supports regex matching as well.

    One more hint – mapping tables can be nested – that way you can look at multiple keys to reproduce nested if-else structures in a visual and scalable way.

    Addendum

    With those events, you can try out the setup on your website (I know, we all love copy-paste):

    adobeDataLayer.push({
        event: "pageload",
        page: {
            name: "Homepage",
            language: "en",
            category: "Home"
        }
    });
    
    adobeDataLayer.push({
        event: "cta-click",
        eventInfo: {
            ctaLabel: "Switch to ACDL now!"
        }
    });
    
    adobeDataLayer.push({
        event: "app-download",
        eventInfo: {
            appDownloadOs: "Android"
        }
    });
    

13 thoughts on “Lean Analytics with the Adobe Client Data Layer

  1. Ben, Jan,

    Could you share your best practices on how you overwrite arrays?

    Here is one of my scenarios. The page contains a facet / search filter with various groups with multiple items in each. Every time a visitor applies the filter by ticking boxes or by selecting values in drop-downs, the DL should be updated (and a beacon should be sent) with the current mix of selected items.

    For simplicity, let’s say that the first DL call may look as follows (with 3 items preselected):

    window.adobeDataLayer.push({
        "event": "click",
        "data": {
            "items": [
                'item1',
                'item2',
                'item3'
            ]
        }
    });
    

    then the visitor changes the filter by unselecting item1 and item3 and selecting item4. In this case, there should be two DL calls 1) to reset the array to null 2) to set the array with the selected items.

    window.adobeDataLayer.push({
        "data": {
            "items": null
        }
    });
    window.adobeDataLayer.push({
        "event": "click",
        "data": {
            "items": [
                'item2',
                'item4'
            ]
        }
    });
    

    Is there a way to make this in a single DL call to just overwrite the array with the new values?

    Another challenge is when an item should be appended to the array. With the approach mentioned above this requires to first read the DL and then push a new item with the just read items to avoid the first one to be overwritten.
    In other words, if now item5 should be appended, the developer can’t just fire the DL as follows

    window.adobeDataLayer.push({
        "event": "click",
        "data": {
            "items": [
                'item5'
            ]
        }
    });
    

    since this would result in item5 and item4 to be returned by getState()

    How do you approach this type of scenario? For example, could you share what you do when a product gets added to the basket?

    Like

    1. Hi Andrey

      Thanks for reaching out, those are valid questions and I’m happy to share my view on those points.

      Regarding the issue of having to reset the data layer array in order to track the current selection of the filter, I’d suggest you to use the eventInfo property.
      By doing so, the values are not persisted in the state of the ACDL and only available within the respective event.
      This means, you don’t have to explicitly clear the array before pushing to it.
      In Launch you can then simply access this array and track the applied filters.

      window.adobeDataLayer.push({
          "event": "click",
          "eventInfo": {
              "items": [
                  'item1',
                  'item2',
                  'item3'
              ]
          }
      });
      
      window.adobeDataLayer.push({
          "event": "click",
          "eventInfo": {
              "items": [
                  'item2',
                  'item4'
              ]
          }
      });
      

      Regarding the issue of appending an element to an existing array, this is indeed not possible in the way you’ve illustrated it. I guess the easiest way of maintaining a consistent data layer would be to use eventInfo like mentioned above.
      For example, you’d then have to push only one product in case of an add-to-basket event or multiple ones in case of a basket.

      If this information should be persisted into the state for potential other tracking requests or services, I’m afraid you would either have to first read the data layer and then update the array, or reset it with null and then push the desired list of products into it.

      I hope this gave you some more insights.
      Let me know if you have any further questions.

      Best regards & have a nice day
      Ben

      Like

  2. hi Jan, Ben,

    I’m working with the GTM data layer and Launch. GTM DL is an event queue and a small Google helper library is added to give it the same functionality you describe above for the ADDL, pushing, merging etc. so making use in Launch seems quite simple.

    https://github.com/google/data-layer-helper

    Data elements are custom JS snippets in Launch return helper.get(‘pageName’);

    Event listeners listen for pushes to the DL and this can be caught in Launch as needed to fire, in my case, websdk edge requests.

    Except for the obvious point that your EDDL launch plugin makes this neater and quicker, do you see any disadvantage to this approach?

    Like

    1. Hi Jan, Ben, Rob

      Rob and I were playing around a bit and noticed something interesting and would like to hear your opinion on this. So basically, would it be possible to have GTM and Launch using the _same_ data layer instance / object and each reacting to the same events in their own way?

      So the setup is as follows
      – GTM uses the standard dataLayer instance name
      – Launch with ACDL extension and “dataLayer” instead of “adobeDataLayer” instance name, plus keeping the “inject ACDL library if not present” set

      What we noticed is that this will somehow still recreate an adobeDataLayer instance resp. point to the dataLayer object (equality)

      In this case, anything that gets pushed at some point into the dataLayer/adobeDataLayer gets processed by both Launch and GTM which makes it basically shareable without any custom code.

      Now the big question is: is this bound to break in a bigger scope? Right now we’ve only used it as small POC. And why is the adobeDataLayer reference anyway recreated even though the instance name is dataLayer? Guess that’s by design and might only be used inside the extension?

      Would be great to have your two cents on this 🙂

      Cheers
      Björn

      Like

      1. Hi Björn, good to hear from you.

        Interesting setup indeed!
        What I’ve done in the past, was to hook into GTM (override the push method) to duplicate all relevant events into ACDL.

        Using only a single data layer object for two distinct tag managers could create some challenges:
        It could happen, that GTM is overwriting some ACDL event listeners and vice versa.
        Another thing to keep in mind, is how the computed state is derived – this is probably also different from GTM to ACDL.
        In theory, your approach might work, although you will have to make sure that GTM and ACDL are in sync, especially when they are initialized.

        To answer your question how and why the adobeDataLayer object is created, I’m adding (parts of) the respective code, which is executed when the ACDL extension is loaded.
        I’m curious to hear more about this dual-setup and how it scales, so please keep me posted 🙂
        Cheers, Ben

        /* if adobeDataLayer is not an object - change it */
        if ('object' !== typeof (window.adobeDataLayer)) {
            turbine.logger.log('Initializing adobeDataLayer to [] as it isn\'t an object yet.');
            window.adobeDataLayer = [];
        }
        
        /* if a new name has been passed, ask adobeDataLayer to use it*/
        if ('adobeDataLayer' !== dataLayerName) {
            if(typeof window[dataLayerName] === 'object') {
                turbine.logger.log('Using "' + dataLayerName + '" instead of "adobeDataLayer" as data layer name.');
                window.adobeDataLayer.push(function (dl) {
                    // push existing events from window[dataLayerName] to adobeDataLayer
                    for (var i = 0; i < window[dataLayerName].length; i++) {
                        dl.push(window[dataLayerName][i]);
                    }
        
                    // set the reference of the existing data layer object to the new one
                    window[dataLayerName] = dl
                });
            } else {
                turbine.logger.warn('Could not use "' + dataLayerName + '" instead of "adobeDataLayer", because the object does not exist!');
            }
        }
        

        Like

    2. Hi Rob, thanks for your comment!

      I appreciate that you already pointed out the most obvious and in my opinion most important point – usability.
      Using the ACDL and its respective Launch extension reduces complexity, avoids a lot of custom JavaScript code, and enables a broader audience to use and review the setup.
      One more thing to keep in mind, is that ACDL is for sure the Adobe preferred technology.
      It is the default out-of-the-box data layer in an AEM setup, it works nicely with the new Web SDK and it is used across the Adobe documentation.

      From a pure technical perspective, both approaches are similar and I don’t see any reason why it wouldn’t work.
      I’m happy to hear more about your approach once you deployed and scaled it in production.

      Cheers, Ben

      Like

      1. Hi Ben

        thanks for the response. I fully agree on the potential challenges/side-effects that reusing the same data layer object may come with and will at least for now put it on the side.

        Curious what Rob’s extension will look like 🙂

        Cheers
        Björn

        P.S. been using the ACDL at a client (you’ve been helping Thomas there, so you know who) and couldn’t be happier with the approach so will definitely try to make it a standard for future implementations

        Like

  3. hi Ben, thanks for the reply and feedback, and I apologise for the slow response. The idea of GTM DL->Launch->AEP is still alive and I’ve created a beta launch extension which will be taken further pending a sanity check with an ACS AA/AT consultant on the same project. The question “why would I use this” is something that would need to be added in bold across any extension as I agree that in most cases the preferred DL solution would be the ACDL.

    Like

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 )

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.