Skip to content
jckautzmann edited this page Feb 17, 2021 · 54 revisions

Adobe Client Data Layer


Introduction

The Adobe Client Data Layer reduces the effort to instrument websites by providing a standardized method to expose and access any kind of data for any script. It consists of a JavaScript client-side event-driven data store that can be used on web pages:

  • to collect data about what the visitors experience on the web page;
  • to communicate this data to digital analytics and reporting servers.

This document focuses on the data layer object API, and not on the data format used to persist the data within the data layer. The data layer imposes no specific data format, but Adobe recommends to use XDM to ease interoperability.


Description

The Adobe Client Data Layer is a JavaScript store for data and events happening on a page within the scope of a request. It provides an API to:

  • Register data that is to be merged into the data layer state.
  • Trigger events that relate to the data stored in the data layer.
  • Get the current data layer state of all merged data.
  • Register listeners that are called for specific events or data changes.
  • List the history of registered data and triggered events.

Architecture

The registration and triggering of events are done by appending the corresponding instructions to a plain JavaScript Array that serves as a queue. This offers multiple benefits:

  • The page and other scripts can start to register data, to trigger events, and to register callbacks by adding them to the array, even before the data layer script itself has been loaded. This allows to load the data layer script asynchronously or deferred, ensuring that it won't affect the page load speed significantly.
  • The data layer array serves as a first-in-first-out queue that avoids racing conditions between scripts and offers at all times the full history of what happened. This allows to easily replay and debug what happened.

Once the data layer script has loaded, it will start with processing each entry in the order that has been added to the array. After that point, the array queue continues to be used to interact with the data layer, but everything that gets added to it will be executed without delay by the data layer.

Technically, the data layer script decorates the data layer's plain JavaScipt Array object with its own API methods, and it will wrap the push() method of the data layer array, so that it can immediately process the appended items.


Setup

To start interacting with the data layer immediately, even before its script has been loaded, it is required to declare the adobeDataLayer array:

window.adobeDataLayer = window.adobeDataLayer || [];

After that point, it is possible to interact with the data layer by always using the adobeDataLayer.push() method to append the instruction to the queue:

window.adobeDataLayer.push({ "test": "Hello World" });

To load the data layer script, which will do the processing of what has been added to the adobeDataLayer array, the Adobe Client Data Layer script must be loaded. This can be done in many ways, including loading it from Adobe Launch or embedding it in a file with other scripts. One recommended way to do it, that has a minimal impact on page load speed, and yet executes the script as quickly as possible, is to use the defer and async attributes (defer takes precedence, but some older browsers only support async):

<script src="adobe-client-data-layer.min.js" defer async></script>

Putting it all together:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>My Page</title>
        <link rel="stylesheet" href="css/mystyles.css">
        <script src="js/myscripts.js" defer></script>
        <script>
            window.adobeDataLayer = window.adobeDataLayer || [];
            window.adobeDataLayer.push({ "test1": "Hello World" });
        </script>
        <script src="js/adobe-client-data-layer.min.js" async defer></script>
    </head>
    <body>
        <h1>My Page</h1>
    </body>
</html>

And inside js/myscripts.js:

window.adobeDataLayer = window.adobeDataLayer || [];
window.adobeDataLayer.push({ "test2": "Hi Again" });

Methods

The following methods are available on the data layer object:

  • push(): Adds items to the data layer.
  • getState(): Returns the merged state of all pushed data.
  • addEventListener(): Sets up a function that will be called whenever the specified event is triggered.
  • removeEventListener(): Removes an event listener previously registered with addEventListener().

Native JavaScript array methods that modify an array (e.g. copyWithin(), fill(), pop(), reverse(), shift(), sort(), splice(), or unshift()) should not be used to modifiy the data layer array as it might lead to inconstistent behaviours of the data layer.

It is important to note that when the data layer is loaded asynchronously, only the push() method can be relied on to exist. For this reason, the API methods that are specific to the data layer, like getState(), addEventListener() and removeEventListener() should only be accessed through a callback that ensures the data layer has properly initialized:

window.adobeDataLayer = window.adobeDataLayer || [];
window.adobeDataLayer.push(function(dl) {
    dl.getState();
});

push()

JSDoc:

/**
 * Adds items to the data layer.
 * 
 * @param {...Object|Function} var_args The items to add to the Data Layer.
 * @returns {Number} The length of the Data Layer array after adding the items.
 */
adobeDataLayer.push(var_args)

The data layer push() method is the only API method that can be used before the data layer script finished loading. For this reason, it is the central method to interact with the data layer. This method is the standard JavaScript Array.prototype.push() method, but when loaded, the data layer will then augment it so that it can process everything that gets pushed to the array without delay.

Two kinds of JavaScript objects can be passed to the push() method:

  • Object: Allows passing data or events that are to be added to the data layer.
  • Function: Allows passing callbacks that the data layer calls immediately, or as soon as it has initialized. When processed, the data layer will remove these items from the queue.
  • Any other object type will trigger an error message in the console once the data layer processes them, and it will be removed from the array queue.

Note that once processed by the data layer, only the Object items will be left in the adobeDataLayer array to easily replay what happened. All other kinds of items will be removed to keep the queue clean and lean.

Pushing a Data Object

Data is added to the data layer by pushing JavaScript Object items to the data layer. This can be done in one big push, or in multiple small ones (keep in mind that the data format used in the examples is purely illustrative at this stage):

window.adobeDataLayer = window.adobeDataLayer || [];
window.adobeDataLayer.push({
    "page": {
        "title": "Getting Started"
    }
});
window.adobeDataLayer = window.adobeDataLayer || [];
window.adobeDataLayer.push({
    "component": {
        "hero-1": {
            "title": "Learn More",
            "link": "learn-more.html"
        }
    }
});
window.adobeDataLayer = window.adobeDataLayer || [];
window.adobeDataLayer.push({
    "component": {
        "accordion-1": {
            "title": "Step by step",
            "shownItems": [
                "component.accordion-1-item-1"
            ]
        },
        "accordion-1-item-1": {
            "title": "Step One",
            "parent": "component.accordion-1"
        },
        "accordion-1-item-2": {
            "title": "Step Two",
            "parent": "component.accordion-1"
        }
    }
});

The multiple pushed items can later be retrieved as one big merged data object with getState().

Pushing an Event Object

Events are also triggered by pushing JavaScript Object items to the data layer, but event objects have a specific event key that will trigger the corresponding event when registered. Events can also provide additional information through the eventInfo key which is optional, or append other data to the data layer (keep in mind that the data format used in the examples is purely illustrative at this stage):

window.adobeDataLayer = window.adobeDataLayer || [];
window.adobeDataLayer.push({
    "event": "page loaded"
});
window.adobeDataLayer = window.adobeDataLayer || [];
window.adobeDataLayer.push({
    "event": "lazy loaded",
    "eventInfo": {
        "reference": "component.map-1"
    },
    "component": {
        "map-1": {
            "position": "here or there…"
        }
    }
});
window.adobeDataLayer = window.adobeDataLayer || [];
window.adobeDataLayer.push({
    "event": "show",
    "eventInfo": {
        "reference": "component.accordion-1-item-2"
    },
    "component": {
        "accordion-1": {
            "shownItems": [
                "component.accordion-1-item-1",
                "component.accordion-1-item-2"
            ]
        }
    }
});
window.adobeDataLayer = window.adobeDataLayer || [];
window.adobeDataLayer.push({
    "event": "click",
    "eventInfo": {
        "reference": "component.hero-1"
    }
});

Note:

  • event and eventInfo can be retrieved as an argument of a registered event handler (see below).
  • event and eventInfo are neither persisted in the data layer array nor in its state.

Pushing an Object to Delete Data

Deleting a data layer entry can be done by pushing a null or undefined value to that key of the data layer.

Example to delete the previously added component.map-1 entry and one of the shownItems:

window.adobeDataLayer = window.adobeDataLayer || [];
window.adobeDataLayer.push({
    "component": {
        "map-1": null,
        "accordion-1": {
            "shownItems": [
                "component.accordion-1-item-1",
                undefined
            ]
        }
    }
});

The new state will look as follows:

{
    "component": {
        "accordion-1": {
            "shownItems": [
                "component.accordion-1-item-1"
            ]
        }
    }
}

Pushing an Object to Update an Existing Array

One important detail to clarify is what happens when pushing data to an already existing array: it will overwrite the data of the inner array.

In the example of the component.accordion-1.showItems property that is an array that contains one item. If we want to modify it to contain a second item, one could push the following into the data layer:

window.adobeDataLayer = window.adobeDataLayer || [];
window.adobeDataLayer.push({
    "component": {
        "accordion-1": {
            "shownItems": [
                "component.accordion-1-item-1",
                "component.accordion-1-item-2"
            ]
        }
    }
});

The new state will look as follows:

{
    "component": {
        "accordion-1": {
            "shownItems": [
                "component.accordion-1-item-1",
                "component.accordion-1-item-2"
            ]
        }
    }
}

Pushing a Function

A JavaScript Function can also be pushed to the data layer, and the data layer calls them immediately, or as soon as it has initialized. This allows to code defensively when not knowing if the data layer has finished loading or not, typically when loading it asynchronously or as a deferred script.

For convenience, the callback function gets a reference to the data layer as parameter that can then be used within the callback to easily access the data layer.

window.adobeDataLayer = window.adobeDataLayer || [];
var myHandler = function(event) {
    console.log(event);
};
window.adobeDataLayer.push(function(dl) {
    dl.getState();
    dl.addEventListener("click", myHandler);
});

This is the recommended way to access the API methods that are specific to the data layer, like getState(), addEventListener() and removeEventListener().

Once processed, the data layer will remove all Function items from the array to keep the queue clean and lean.


getState()

JSDoc:

/**
 * Returns the merged state of all pushed data.
 * 
 * @param {String} [reference] Optional path of the part of the state to get.
 * @returns {Object} The state object.
 */
adobeDataLayer.getState(reference)

When calling the getState() method without any parameter, it returns the entire merged state of all pushed data:

window.adobeDataLayer = window.adobeDataLayer || [];
window.adobeDataLayer.push(function(dl) {
    var state = dl.getState();
    console.log(state);
});

Considering the previous examples of this documentation, the above code would then log the following:

{
    "page": {
        "title": "Getting Started"
    },
    "component": {
        "hero-1": {
            "title": "Learn More",
            "link": "learn-more.html"
        },
        "accordion-1": {
            "title": "Step by step",
            "shownItems": [
                "component.accordion-1-item-2"
            ]
        },
        "accordion-1-item-1": {},
        "accordion-1-item-2": {}
    }
}

Optionally, a string can be passed to the getState() method to retrieve specific data. This string supports the dot notation to specify an object path:

window.adobeDataLayer = window.adobeDataLayer || [];
window.adobeDataLayer.push(function(dl) {
    var state = dl.getState("component.hero-1");
    console.log(state);
});

Which would then log:

{
    "title": "Learn More",
    "link": "learn-more.html"
}

addEventListener()

JSDoc:

/**
 * Event listener callback.
 *
 * @callback eventListenerCallback A function that is called when the event of the
 *     specified type occurs.
 * @this {DataLayer}
 * @param {Object} event The event object pushed to the data layer that triggered
 *     the callback.
 */

/**
 * Sets up a function that will be called whenever the specified event is triggered.
 *
 * @param {String} type A string representing the event type to listen for.
 * @param {eventListenerCallback} callback A function that is called when the event
 *     of the specified type occurs.
 * @param {Object} [options] Optional characteristics of the event listener.
 * @param {String} [options.path] Filters events by state object path.
 * @param {('past'|'future'|'all')} [options.scope] Filters events by timing:
 *      - {String} past The listener is triggered for past events only.
 *      - {String} future The listener is triggered for future events only.
 *      - {String} all The listener is triggered for past & future events (default).
 */
adobeDataLayer.addEventListener(type, listener, options);

Example to define a callback function that is called for the specified "click" event:

window.adobeDataLayer = window.adobeDataLayer || [];
var myHandler = function(event) {
    console.log(event);
};
window.adobeDataLayer.push(function(dl) {
    dl.addEventListener("click", myHandler);
});

Note 1: you should pass a reference to a function as the listener argument, not an anonymous function. To remove the listener you just pass the same reference to the removeEventListener method. If you pass an anonymous function, you won't be able to unregister it: this behavior is similar to the unregistering of a DOM event listener (EventTarget.removeEventListener()).

Note 2: you should make sure that the logic of your listener handler does not end up in an endless loop. For example pushing an event within a handler that is registered against the same event might lead to an endless loop.

One particularity about data layer event listeners is that they also get called for previously triggered events. This allows to avoid racing conditions for events that might have been fired before other scripts could register their listeners. Therefore, the above script will still catch the click event that happened earlier and log the following:

{
    "event": "click",
    "eventInfo": {
        "reference": "component.hero-1"
    }
}

Additionally, the data layer provides a few special events:

  • adobeDataLayer:change: Triggered when data is pushed to the data layer.
  • adobeDataLayer:event: Triggered when an event is pushed to the data layer.

The last options parameter allows specifying a path reference to filter so that only events that contain this path get notified. In the future, more options might be added.

Example to get all data changes for a specific object of the data layer, thanks to the path option. This string supports the dot notation to specify an object path:

window.adobeDataLayer = window.adobeDataLayer || [];
var myHandler = function(event) {
    console.log(event);
};
window.adobeDataLayer.push(function(dl) {
    dl.addEventListener(
        "adobeDataLayer:change", 
        myHandler, 
        {"path": "component.accordion-1"}
    );
});

Which will log the initial appending of that component to the data layer, and the subsequent event modifying it:

{
    "component": {
        "accordion-1": {
            "title": "Step by step",
            "shownItems": [
                "component.accordion-1-item-1"
            ]
        },
        "accordion-1-item-1": {
            "title": "Step One",
            "parent": "component.accordion-1"
        },
        "accordion-1-item-2": {
            "title": "Step Two",
            "parent": "component.accordion-1"
        }
    }
}
{
    "event": "show",
    "eventInfo": {
        "reference": "component.accordion-1-item-2"
    },
    "component": {
        "accordion-1": {
            "shownItems": [
                "component.accordion-1-item-1",
                "component.accordion-1-item-2"
            ]
        }
    }
}

Listening to data changes for a specific object would also return entries where the corresponding object gets deleted.

Example, deleting the accordion entirely:

window.adobeDataLayer = window.adobeDataLayer || [];
window.adobeDataLayer.push({
    "component": {
        "accordion-1": null,
        "accordion-1-item-1": null,
        "accordion-1-item-2": null
    }
});

This would also cause the previously registered event to be called and to log this change.

{
    "component": {
        "accordion-1": null,
        "accordion-1-item-1": null,
        "accordion-1-item-2": null
    }
}

Note: the callback gets also triggered when an ancestor object gets deleted. See an example.


removeEventListener()

JSDoc:

/**
 * Removes an event listener previously registered with addEventListener().
 * 
 * @param {String} type A case-sensitive string representing the event type to listen for.
 * @param {Function} [listener] Optional function that is to be removed.
 */
adobeDataLayer.removeEventListener(type, listener);

Example to unregister all listeners for the adobeDataLayer:change event:

window.adobeDataLayer = window.adobeDataLayer || [];
window.adobeDataLayer.push(function(dl) {
    dl.removeEventListener("adobeDataLayer:change");
});

Example to unregister a specific listener for the click event:

window.adobeDataLayer = window.adobeDataLayer || [];
window.adobeDataLayer.push(function(dl) {
    dl.removeEventListener("click", myHandler);
});