Web Analytics with Adobe’s Customer Journey Analytics, Part 4: Capturing Data with Web SDK (Alloy)

This post is the fourth post of the eight-part-series Web Analytics with Adobe’s Customer Journey Analytics, showing how web sites can be analyzed better using Adobe’s next evolution of Adobe Analytics. In the previous post, we took a look at our business questions and how we can structure our data most effectively. In this post, we are doing the actual implementation using Adobe Launch, the Adobe Web SDK, and Client Data Layer.

On our way to creating a full-scope, front-to-back implementation of Customer Journey Analytics to track a web site, we are now ready to think about our actual implementation. Since we have the data structure in place and already have an awesome Experience Event Schema, we just need some actual data.

The logical choice to feed data to the Adobe stack is, of course, by utilizing their client-side tools as well. Specifically, we are going to use Adobe Launch as the tag manager with Adobe’s brand new Web SDK extension (formerly known as Alloy). Doing that brings a lot of convenience and future-proveness, since both tools are perfectly integrated with the rest of the Adobe ecosystem.

To feed data to the Web SDK, we can go down two routes: We can either use the (rather dated) approach of defining a data layer as a custom java script object as base information and use custom events to integrate the site with analytics, or use the more modern Event Driven Data Layer (EDDL) approach with Adobe’s Client Data Layer extension in Launch. Both approaches have their pros and cons:

JS Data Layer + Custom Events
Pros
  • Well understood standard
  • Very developer-friendly
  • Works with virtually ever site and tool in existence
Cons
  • Dated approach
  • Requires a lot of data mapping
  • Data layer does not interact with events
  • May introduce race conditions
EDDL
Pros
  • Modern approach
  • Growing adoption
  • Lean design
  • Solves race-condition headaches
Cons
  • Lesser known standard
  • Many different implementations
  • Hard to govern and get right across big websites if not documented well

Since this series is somewhat about bringing our analytics stack to the modern day, we are going to use the EDDL/Adobe Client Data Layer approach. I have used it before for a previous post and it worked like a charm back then, so why not take it for another spin? Just take a mental note that the same could be done using any other approach.

Preparing the Environment

After we have created the XDM Schema in the previous post, we now need to create a dataset in Experience Platform with that schema and create a Datastream in Launch referencing that dataset. To do that, open Launch and head over to the Datastreams section. My config looks like this:

Connecting Launch to Experience Platform

With the Datastream in place, we can now add all the extensions we need to our Launch property. For our basic analytics implementation, we only need a few of those extensions:

Web SDK properties look very neat and tidy

In terms of preparing the environment, that’s already all we need! Now we can start creating the Data Elements we need.

Setting up the Data Elements

We will start with some information about the Launch environment. Unfortunately, we can’t just use a pre-made Data Element for them but have to collect them using custom code Data Elements (make sure to up-vote my Pull Request if you want a more convenient way) or JS variables, so we need to create those Data Elements ourselves. This is how I named my Data Elements together with the values they return:

  • Library Build Date: _satellite.buildInfo.buildDate
  • Launch Environment Stage: _satellite.buildInfo.environment
  • Launch Property Name: _satellite.property.name
  • Event ID: event.identifier
  • Rule ID: event.$rule.id
  • Rule Name: event.$rule.name
  • Rule Type: event.$type

Next, create a Data Element from the Adobe Client Data Layer extension for the Computed State. Unlike the previous post, we are not going to track the before- and after-states of the data layer. However, we still need to create yet another custom code Data Element for the event message (why, Adobe?! At least open-source the extension so we can create pull requests for those simple additions!) returning this information for each:

  • Datalayer Event Info: event.message.eventInfo
  • Datalayer Event Identifier: event.message.event

With those basic things done, we need to create the actual XDM Data Element. For that, we can go down three routes:

  1. If we want to only have one XDM Data Element, we would need to implement a bit of custom logic in code to merge the data layer state with the event object, since the Web SDK extension will not let us simultaneously define a whole section of the XDM as one Data Element while having parts of that section filled from other Data Elements. Right now, there is no way to do that without custom code with just the Web SDK and Core extensions installed.
  2. To circumvent the limitation outlined above, we could also only reference parts of the computed state and event object in a lot of Data Elements (a lot of them with custom code) and still stick to only one XDM Data Element for all rules.
  3. Alternatively, we could just create multiple XDM Data Elements (one for each type of event), which might lead to some maintenance overhead over time, since we would then potentially need to add new information from the data layer to multiple Data Elements if we want to track it everywhere.
  4. While I don’t think people should need to do this, there is still one more option: Change the XDM schema so we can separate information from the computed state and event object. Technically speaking, that would solve all issues outlined above, but would destroy the neat and tidy structure we have built (as the page name would probably live in the computed state, while page views will be in the event object).

Right now, we are going to follow option 1. I can’t say I particularly like it, but that is kind of true for all available options. I think Adobe needs to solve this across their products (AEP, Launch, Web SDK, ACDL) to make live easy and sustainable for us customers. But let’s move on for now.

Mapping the Data Elements

First, let’s get the technical variables populated first. In our XDM Data Element, I have already mapped the Data Elements described above to the dimensions we have defined in the XDM schema:

Mapping the technical dimensions manually

That is the easy part. But now it gets a bit tricky:

Addressing conflicts between Web SDK and XDM

So the most convenient way would be to set the whole web object with a single Data Element. But that would mean we had to set things like the URL and Page Name explicitly with every event, except we merge it with the data layer computed state manually. Fortunately, I have discovered some great functionality in the Web SDK extension: Even if we set the whole web object with a single Data Element the extension still sets things like the URL automatically for us! Whew, that’s a big relief!

Knowing that, we can keep our choice of approach. In Launch, we create a new Data Element with (again, I hate it) quite a bit of custom code that I based off of this blog post:

var extend = function () {

    // Variables
    var extended = {};
    var deep = false;
    var i = 0;

    // Check if a deep merge
    if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) {
        deep = arguments[0];
        i++;
    }

    // Merge the object into the extended object
    var merge = function (obj) {
        for (var prop in obj) {
            if (obj.hasOwnProperty(prop)) {
                // If property is an object, merge properties
                if (deep && Object.prototype.toString.call(obj[prop]) === '[object Object]') {
                    extended[prop] = extend(extended[prop], obj[prop]);
                } else {
                    extended[prop] = obj[prop];
                }
            }
        }
    };

    // Loop through each object and conduct a merge
    for (; i < arguments.length; i++) {
        merge(arguments[i]);
    }

    return extended;

};

return extend(true, _satellite.getVar("Data Layer Computed State"), _satellite.getVar("Datalayer Event Info",event));

Let’s take it slow and analyze what this actually does: We are taking the computed state as base information and add the Event Info to that. By defining it like that, keys that exist in both objects will give precedence to the Event Info, which makes a lot of sense. Through this approach we could, for example, overwrite the page name from the data layer state with what we get from the event itself. Another thing that I discovered while testing is that the Web SDK sets its variables after all other, so that everything we would manually set would be ignored. I personally don’t like that, but it’s not critical for our use case, so we will move on for now.

Tracking data

With all that prep work done, all we need is a simple rule to fire on the data layer push for an event, like this:

Firing our Launch rule on data layer events

Now it’s time to actually start tracking! After all the preparation, we can track our data in a very sensible way. First, let’s look at how we can push data to the data layer that should be persisted to later events:

adobeDataLayer.push(
    {
        "webPageDetails":{
            "name": "Home Page",
            "_yourCompany":{
                "navigation_method": "Top Menu",
                "performance":{
                    "total_load_time":2
                }
            }
        }
    }
)

That part should be pretty clear. We are adding the name of the page and its load time to the state, since that information will not change later on. Note that we are not pushing content impressions yet, since adding those to the state would mean we either track an impression with every following event or we would have to clear out the impressions afterwards. I don’t want that, so I’ll add it later.

Next, let’s look at how we would track a page view together with the content impressions:

adobeDataLayer.push(
    {
        "event":"Page View",
        "eventInfo":{
            "webPageDetails":{
                "pageViews":{
                    "id": "Page View",
                    "value":1
                },
                "_yourCompany":{
                    "content":[
                        {
                            "name":"Hero Teaser",
                            "path":"Page > Hero Teaser",
                            "type":"Teaser",
                            "interactions":{
                                "impressions":1
                            },
                            "position":{
                                "number":1
                            },
                            "interactive_elements":[
                                {
                                    "name":"Call to action",
                                    "type": "Button",
                                    "position":{
                                        "column":1,
                                        "row":1,
                                        "number":1
                                    },
                                    "interactions":{
                                        "impressions":1
                                    }
                                }
                            ]
                        },
                        {
                            "name":"Article Gallery",
                            "path":"Page > Article Gallery",
                            "type":"Gallery",
                            "interactions":{
                                "impressions":1
                            },
                            "position":{
                                "number":2
                            },
                            "interactive_elements":[
                                {
                                    "name":"Article Teaser: New exciting content",
                                    "type": "Teaser",
                                    "position":{
                                        "column":1,
                                        "row":1,
                                        "number":1
                                    },
                                    "interactions":{
                                        "impressions":1
                                    }
                                },
                                {
                                    "name":"Article Teaser: Old, less exciting content",
                                    "type": "Teaser",
                                    "position":{
                                        "column":1,
                                        "row":1,
                                        "number":2
                                    }
                                }
                            ]
                        }
                    ]
                }
            }
        }
    }
)

In the first 8 lines, we are incrementing the actual page view by setting the counter to 1. After that, we are tracking impressions on the actual page content. For our demo, I’m tracking two page elements (a hero teaser and an article gallery) with their respective interactive elements. By doing it this way, you can see how I’m not incrementing the impressions for the second teaser in the article gallery. If I wanted to do that (for example, if the user clicked on the gallery to slide to the next item), I could do it like this:

adobeDataLayer.push(
    {
        "event":"Content Interaction",
        "eventInfo":{
            "webPageDetails":{
                "_yourCompany":{
                    "content":[
                        {
                            "name":"Article Gallery",
                            "path":"Page > Article Gallery",
                            "type":"Gallery",
                            "interactions":{
                                "interactions":1
                            },
                            "position":{
                                "number":2
                            },
                            "interactive_elements":[
                                {
                                    "name":"Article Teaser: New exciting content",
                                    "type": "Teaser",
                                    "position":{
                                        "column":1,
                                        "row":1,
                                        "number":1
                                    },
                                    "interactions":{
                                        "swipe_from": 1,
                                        "impressions": 0,
                                        "swipe_to": 0
                                    }
                                },
                                {
                                    "name":"Article Teaser: Old, less exciting content",
                                    "type": "Teaser",
                                    "position":{
                                        "column":1,
                                        "row":1,
                                        "number":2
                                    },
                                    "interactions":{
                                        "swipe_from": 0,
                                        "impressions": 1,
                                        "swipe_to": 1
                                    }
                                }
                            ]
                        }
                    ]
                }
            }
        }
    }
)

As you can see in line 13, I’m now tracking an interaction for the Article Gallery, while tracking an impression for the second teaser and swiping interactions for both teasers. Pretty awesome, right?

Conclusion

We got a lot of things done in this post. After setting up the environment, we successfully merged data coming from the Adobe Client Data Layer into a single object and tracked it with the Web SDK. That’s super neat! Doing this myself, I really felt like this new way of structuring our data is much more natural and closer to how it actually should be. Sure, there are a few things to consider before you can even start, but the results kind of speak for themselves I’d say.

That’s a good result for today. In the next post, we are going to crunch our data even more using Query Service in Experience Platform. If you were wondering how we will get our previous page field populated: That’s how. See you there!