Sticky

Engineers Love This One Weird (Google Data Layer) Trick!

  • 2 June 2023
  • 2 replies
  • 363 views
Engineers Love This One Weird (Google Data Layer) Trick!
Userlevel 5
Badge +3

Ed note: This article was updated on June 11, 2023 to include code that listens to gtag() calls as well as dataLayer.push()

Google’s Universal Analytics stops collecting data in one month, on July 1, 2023. Also, recent rulings related to Google Analytics and HIPAA has health care services providers scrambling for a HIPAA compliant alternative. So making it easy for these people to transition has been top of mind for me.

 

If you’ve already invested in building out your Google Data Layer, it can feel like a pain to ask your engineers to re-implement event tracking and enrichment for another tool like Heap. There are two ways to get this data into Heap without re-work:

  1. Using Google Tag Manager (something I plan to write a post on another time, let me know in the comments if you want to see this sooner than later)
  2. “Hijacking” the call to dataLayer.push() (which is what your engineers do to put data in it). That’s what this blog post is about.

Here’s the basic idea:

var originalPush = dataLayer.push;

dataLayer.push = function(obj) {

if (obj.event) {
heap.track(obj.event, flattenObject(obj))
}

originalPush.call(this, obj);
};

Very straight-forward. Whenever an event is pushed to the dataLayer, also send it to Heap.

What about user- or page-level metadata? Often those are stored in an object called user or page. They are also often pushed to the dataLayer object before Google Tag Manager is loaded and before Heap is loaded.

Here’s some code to handle that; we’ll check that they exist, then send them to Heap once loaded if so. (These are usually flat objects, but if your implementation nests objects, just add flattenObject, which I say more about below.) Note that here I’m adding the user properties as event properties, too. This is a common best practice to handle cases where the user properties change over time, like the plan they are on. Let’s also handle the edge case of events being pushed before Heap loads. And handle usage of gtag() in addition to dataLayer.push()

var initialPageProps = dataLayer.find(function(obj) {
return obj.page;
})?.page;

var initialUserProps = dataLayer.find(function(obj) {
return obj.user;
})?.user;

var initialEvents = dataLayer.filter(function(obj) {
return (obj.length > 1 && obj[0] === 'event' && !obj[1].startsWith('gtm.')) || (obj.hasOwnProperty('event') && !obj.event.startsWith('gtm.'));
});


if (initialPageProps) {
heap.addEventProperties(initialPageProps);
}

if (initialUserProps) {
heap.addEventProperties(initialUserProps);
heap.addUserProperties(initialUserProps);
}

if (initialEvents) {
initialEvents.forEach(function(eventObj) {
var eventName, eventData;
if (eventObj.length > 1) {
eventName = eventObj[1];
eventData = eventObj[2];
} else {
eventName = eventObj.event;
eventData = eventObj;
}
heap.track(eventName, flattenObject(eventData));
});
}

So far, this should be a sufficient solution for a regular website. If you have a Single Page Application, it’s common to simply keep shoving things into the dataLayer and because there’s no page unload/load, the array just keeps on growing. So we need to handle the enrichment whenever it is pushed. 

Let’s add this to the original code.

var originalPush = dataLayer.push;

dataLayer.push = function(obj) {

if (obj.page) {
heap.addEventProperties(obj.page);
}

if (obj.user) {
heap.addEventProperties(obj.user);
heap.addUserProperties(obj.user);
}

if (obj.length > 1 && obj[0] === 'event' && !obj[1].startsWith('gtm.')) {
heap.track(obj[1], flattenObject(obj[2]));
}

if (obj.event) {
heap.track(obj.event, flattenObject(obj))
}

originalPush.call(this, obj);
};

Let’s put it all together, including the function flattenObject which we need to conveniently handle whatever metadata you send with your event. It’s a bit greedy, but you can always archive anything you don’t need in the Heap app.

Here’s the final commented code:

// Create the flattenObject function

function flattenObject(obj) {
var result = {};

function recurse(cur, prop) {
if (Object(cur) !== cur) {
result[prop] = cur;
} else if (Array.isArray(cur)) {
var arr = [];
for (var i = 0, l = cur.length; i < l; i++) {
if (typeof cur[i] === 'object' && !Array.isArray(cur[i])) {
arr.push(JSON.stringify(cur[i]));
} else {
arr.push(cur[i]);
}
}
result[prop] = arr.join(';');
} else {
var isEmpty = true;
for (var p in cur) {
isEmpty = false;
recurse(cur[p], prop ? prop + "." + p : p);
}
if (isEmpty && prop) {
result[prop] = {};
}
}
}

recurse(obj, "");
return result;
}


// Load Heap

<heap snippet here>
heap.load("YOUR_APP_ID_GOES_HERE");


// Check for existing user and page and event data and send it

var initialPageProps = dataLayer.find(function(obj) {
return obj.page;
})?.page;

var initialUserProps = dataLayer.find(function(obj) {
return obj.user;
})?.user;

var initialEvents = dataLayer.filter(function(obj) {
return (obj.length > 1 && obj[0] === 'event' && !obj[1].startsWith('gtm.')) || (obj.hasOwnProperty('event') && !obj.event.startsWith('gtm.'));
});


if (initialPageProps) {
heap.addEventProperties(initialPageProps);
}

if (initialUserProps) {
heap.addEventProperties(initialUserProps);
heap.addUserProperties(initialUserProps);
}

if (initialEvents) {
initialEvents.forEach(function(eventObj) {
var eventName, eventData;
if (eventObj.length > 1) {
eventName = eventObj[1];
eventData = eventObj[2];
} else {
eventName = eventObj.event;
eventData = eventObj;
}
heap.track(eventName, flattenObject(eventData));
});
}


// Cache the original dataLayer.push function (important)

var originalPush = dataLayer.push;


// Modify the function to send data to Heap and then add the data to the dataLayer

dataLayer.push = function(obj) {

if (obj.page) {
heap.addEventProperties(obj.page );
}

if (obj.user) {
heap.addEventProperties(obj.user);
heap.addUserProperties(obj.user);
}

if (obj.length > 1 && obj[0] === 'event' && !obj[1].startsWith('gtm.')) {
heap.track(obj[1], flattenObject(obj[2]));
}

if (obj.event) {
heap.track(obj.event, flattenObject(obj))
}


originalPush.call(this, obj);
};

 

With this one snippet, all your user and page enrichment and event data will be sent to Heap, without any further effort by your engineers. 

Got questions? Leave them in the comments.


2 replies

Userlevel 3
Badge +3

@jonathan This is amazing! And since you mentioned it, we look forward to your Google Tag Manager write-up!

Userlevel 5
Badge +3

Code has been updated to accommodate use of gtag() to send events.

Reply