Adobe Launch Dynamic Data Elements

Overview

One issue I faced at my job was that the data layer architecture was inconsistent across the various site sections. I’ll anonymize this by pretending I work for an airline; if we wanted to track the name of the destination(s) the user was looking at, there were a few main use cases:

  1. The user is selecting (clicking) a specific destination, such as a filter in the booking flow, or a link to a content page about that destination.

  2. The user is viewing search results for flights to a single or multiple destinations.

  3. The user is looking at a content page about the destination itself.

  4. A known user is just browsing around, but they already have a booking to a specific destination. (Knowing this helps us serve personalized content to them as they browse.)

Rather than having lots of diffent ‘destination’ data elements in our codebase, which then have to be mapped into different Launch rules and XDMs, we can create one ‘destination’ data element that checks for each of these use cases in order of priority.

Use Cases

Use Case 1: Modern Data Layer

The modern data layer architecture (which each of our site sections is refactoring to as each gets replatformed) groups the destination code and name together within each property, like this:

dataLayer: {
  booking: {
    destination: {
      code,
      name,
    },
  },
  content: {
    destination: {
      code,
      name,
    },
  },
  search: {
    destination: {
      code,
      name,
    },
  },
}

Use Case 2: Legacy Data Layer

Older site sections use a flatter data layer architecture, which does not nest the code and name together under one destination property:

dataLayer: {
  booking: {
    destination,
    destinationCode,
  },
  content: {
    destination,
    destinationCode,
  },
  search: {
    destination,
    destinationCode,
  },
}

Use Case 3: Data Attributes (Clicks)

When we began implementing AEP as well, we created one global XDM that could be used for both clicks and page/route loads. The reasoning for this was twofold:

  1. To be D.R.Y.

  2. To ensure that the schema architecture is identical in all circumstances, preventing batch ingestion failures.

Therefore, these same dynamic data elements required logic that would first check if the rule-triggering event contained data attributes.

Syntax

Here is the actual programming. (Scroll down for a description of how it works.)

let destinationName;
let destinationNameArr;
let destinationNameMap;

const clickAttributes = (!!event && !!event.detail) ? event.detail : '';

if (
  !!clickAttributes &&
  !!clickAttributes['data-track-noun'] &&
  clickAttributes['data-track-noun'] === 'filter-destination' &&
  !!clickAttributes['data-track-name']
) {
  // Booking Flow Destination Filter Clicks (future-proofed version):
  destinationName = clickAttributes['data-track-name'];
} else if (
  !!clickAttributes &&
  !!clickAttributes['data-track-noun'] &&
  clickAttributes['data-track-noun'] === 'search-filter'
) {
  // Do nothing.
} else if (!!window.dataLayer) {
  if (
    !!window.dataLayer.booking &&
    typeof window.dataLayer.booking.destination === 'object' &&
    typeof window.dataLayer.booking.destination.name === 'string'
  ) {
    destinationName = window.dataLayer.booking.destination.name;
  } else if (
    !!window.dataLayer.booking &&
    typeof window.dataLayer.booking.destination === 'string'
  ) {
    destinationName = window.dataLayer.booking.destination;
  } else if (
    !!window.dataLayer.search &&
    !!window.dataLayer.search.destination &&
    Array.isArray(window.dataLayer.search.destination)
  ) {
    destinationNameArr = window.dataLayer.search.destination;
    destinationNameMap = destinationNameArr.map(destination => destination.name);
    if (!!destinationNameMap && Array.isArray(destinationNameMap)) {
      destinationName = destinationNameMap.toString();
    }
  } else if (
    !!window.dataLayer.search &&
    typeof window.dataLayer.search.destination === 'string'
  ) {
    destinationName = window.dataLayer.search.destination;
  } else if (
    !!window.dataLayer.content &&
    typeof window.dataLayer.content.destination === 'object' &&
    typeof window.dataLayer.content.destination.name === 'string'
  ) {
    destinationName = window.dataLayer.content.destination.name;
  } else if (
    !!window.dataLayer.content &&
    typeof window.dataLayer.content.destination === 'string'
  ) {
    destinationName = window.dataLayer.content.destination;
  }
}

if (
  !!destinationName &&
  typeof destinationName === 'string' &&
  destinationName !== 'NA'
) {
  return destinationName;
}

Explanation

  • Line 1: Declare the variable where the value will be stored. For AEP purposes, do not set any default value. (If there is no value, we want this data element to return undefined so that its property is omitted from the XDM. If it defaults to '' or null, the property will be added to the XDM, which could result in a batch ingestion failure.)

  • Lines 2 - 3: Declare the variables that will be used to handle array use cases later in this flow.

  • Line 5: If the rule that invokes this data element has been triggered by a click, an object will have been passed into it. That object will be the clicked element. We store it in the clickAttributes variable. (And if the event was not a click, we default this variable to an empty string so that its condition will not be met later in our logic.)

  • Lines 7 - 14: The first thing we check are the click use cases. Some of our pages follow the new and improved convention of having data-track-noun="filter-destination" attributes. If a clicked element has this attribute, we’ll want to pull the value from its data-track-name attribute. We check that both exist before attempting to do this.

  • Lines 15 - 20: Some of our pages have search filters whose data attributes have not been updated to the new and improved convention described above. If this is the case, they will have a legacy attribute called data-track-noun="search-filter". If so, they do not also contain a data-track-name attribute, so we can’t pull the destination name from it. Instead, we keep a condition here that will be met but do nothing. This prevents clicks on these legacy elements from falling back to the values in the data layer further down the flow — which would not accurately reflect what was clicked.

  • Lines 21 - 60: We now enter the data layer conditions.

    • Lines 22 - 32: First we check if a booking destination exists.

      • Lines 22 - 27: We first look for the booking property in the modern data layer architecture.

        • Line 24: Rather than just checking to see if window.dataLayer.booking.destination exists, we confirm that it is an object. This is because if our code was reading the legacy data layer, this condition would be met even though it is only a string. If that were the case, attempting to check window.dataLayer.booking.destination.name on the next line would throw an error.

        • Lines 25 - 27: Having confirmed that window.dataLayer.booking.destination is an object, we now confirm that window.dataLayer.booking.destination.name is a string. If so, we store it in our destinationName variable.

      • Lines 28 - 32: If the destination name was not found in the modern data layer architecture, we check the legacy data layer architecture next.

    • Lines 33 - 47: If the destination name was not found in the booking properties, it’s time to check the search properties — first in the modern architecture, then in the legacy architecture.

      • Lines 33 - 36: The modern data layer’s search property is a little more complex because it is an array. The user has the ability to search for multiple destinations at a time. This first condition includes validation to confirm that we are indeed dealing with an array before we try to run methods on a property that will throw an error if it’s not an array.

      • Line 38: Store the array in destinationNameArr. (Each item in the array will contain both a destination name and a destination code.)

      • Line 39: Map each name value into a new array, and store it in destinationNameMap.

      • Lines 40 -42: We do another quick validation check to make sure we aren’t pulling the wrong data type, and then we convert the array into a string.

      • Lines 44 - 47: On the other hand, if we are working with the legacy data layer, the list of destination names will already be a string. And they will not include destination codes.

    • Lines 48 - 59: If the destination name was not found in booking or search, next check content. As always, we check for the modern data layer first, and the legacy data layer second.

      • Lines 50 - 51: It is important to confirm that dataLayer.content.destination is an object before considering it to be a match. This is because the legacy data layer also contains this exact same property, only it is a string and contains the final value. If this is the case and we next try to run some dot notation on it (on line 51), it will throw an error.

  • Lines 63 - 65: To prevent batch ingestion failures in AEP, we make sure no null values, empty strings, or 'NA' default values from the data layer get passed through. Done!