Skip to content

Instantly share code, notes, and snippets.

@samba
Last active May 7, 2025 04:12
Google Tag Manager Utility Scripts

Utility Scripts for Google Tag Manager

Each script should include some outline of its usage. In general, the scripts will require installation as a Custom HTML tag in GTM, and should be triggered to run on every page.

Custom HTML scripts in GTM require wrapping Javascript in <script> tags, like so:

<script>
// INSERT THE JAVASCRIPT CODE HERE
</script>

Within <script> tags it's safe to include Javascript comments, and GTM will automatically strip them out when serving the code to a browser.

Goals:

  • Simplify acquisition of useful interactions and associated data, in GTM
  • Reduce complexity for site-maintainers (i.e. developers) to provide useful context to GTM for analytics.
/** Collect "data context" from inherited elements in a document.
* NOTE: GTM will probably require you to remove all or part of this comment
* before deploying, since it contains text like "{{...}}", which it interprets
* as GTM variable references.
*
* Usage:
* 1. Install this tag as a Custom HTML tag that runs on every page.
* 2. Instruct developers to add data attributes (e.g. data-your-property="value")
* on all elements that should have analytics value.
* 2. Create a "Variable" entry for the auto-generated `dataContext` attribute
* in the dataLayer.
* 3. Create "Variable" entries associated to relevant components of the page.
*
* This plugin now installs itself in the GTM DataLayer event stack (the `push()`
* method) to augment all events containing a valid `gtm.element` property. This
* provides an additional property on all such events, named `dataContext`. You
* can then create DataLayer Variables (v2) inside GTM that refer to properties,
* such as `dataContext.title`, `dataContext.alt`, and `dataContext.yourProperty`.
*
* Example: retrieve data context of a specific header, by CSS selector.
* > Define the GTM Custom Javascript variable "data-module-header-context" as:
* function(){
* var selector = "div.module h2";
* return jQuery(selector).dataContext()[0];
* }
*
* Then, to retreive component parts, additional variables:
* function(){
* var ctx = {{data-module-header-context}};
* // supposing an attribute like 'data-module-type=value'
* var name = "moduleType";
* return ctx[name];
* }
*
* Additional considerations:
* - When an element provides attribute "data-something-else", the related
* data property is "somethingElse" (i.e. camel-cased).
* - When an element provides an attribute _without definition_, the result
* value in context will be an empty string.
*
* Occasionally it may be necessary to retrieve context from a "nearby" element.
* In these cases, jQuery provides fairly simple means to seek them out. In a
* GTM variable, you'd provide it like so:
* function(){
* // example: get data from a "selected" option in a nearby menu.
* var container = 'div.container';
* var seek = 'div.menu ul li.selected';
* var target = jQuery({{ element }}).parents(container).first().find(seek);
* var ctx = target.dataContext()[0];
* return ctx[attribute]; // << define your attribute name too.
* }
*
* This plugin also acquires the 'alt' and 'title' attributes; you can add more
* names to the `interesting_attributes` list below to pull them into context
* by default.
*
* In other Javascript:
* jQuery('div.my.element').dataContext() => Array<Object>
*
* Purpose:
* 1. simplify acquisition of meaningful data on each interaction within GTM.
* 2. reduce complexity for site maintainers to add context for analytics.
*
*
*/
(function($, undefined) {
if (!$) {
console.error("jQuery was not available when the dataContext plugin loaded.");
return false;
}
var debug = false;
var dataLayer_name = "dataLayer";
var interesting_attributes = ['alt', 'title'];
var pending_state_clear = false;
function observer(listen) {
// This will add "context.click" and similar events to GTM, so the same
// properties can be acquired on additional interactions.
listen(document.body, 'click mousedown tapstart');
listen(document.body, 'submit', 'form');
listen(document.body, 'change', 'input, select, textarea');
}
function interceptor(event) {
// adapt DataLayer events nicely.
var node = (event && event['gtm.element']);
var data;
if (node && node.nodeType){
data = $(node).dataContext();
event['dataContext'] = data[0];
pending_state_clear = true;
}
return event;
}
function pendingStateClear(dataLayer){
if(pending_state_clear){
dataLayer.push({'dataContext': undefined});
pending_state_clear = false;
}
}
function getElementText(elem) {
if(debug) console.info('Getting text of element', elem);
return $.trim($(elem).text());
}
function getElementData(elem) {
var set = $(elem);
var data = set.data();
// TODO: temporarily disabled due to bug, acquiring whole document text.
// maybeAddAttribute(data, 'text', getElementText(elem));
interesting_attributes.map(function(v) {
maybeAddAttribute(data, v, set);
});
return data;
}
function maybeAddAttribute(data, name, set) {
var value = strip((typeof set == 'string') ? $.trim(set) : $.trim(set.attr(name)));
if (value) data[name] = value;
else data[name] = undefined;
return data;
}
function strip(text) {
return text.replace(/\s+/g, ' ');
}
function mapParents(elem, callback) {
var result = [];
$(elem).parents().each(function(i, node) {
result.push(callback.call(node, node, i));
});
return result;
}
function getContext(elem) {
var data = getElementData(elem);
var inherited = mapParents(elem, getElementData);
if(debug) console.info('Merging data sets', data, inherited);
inherited.map(function(more) {
data = jQuery.extend(data, more);
});
return data;
}
$.fn.dataContext = function() {
return $.makeArray(this).map(getContext);
};
// Apply listeners for additional events
$(document).ready(function() {
observer(function(context, event, selector) {
$(context).on(event, selector, function(e) {
observer.dataLayer.push({
'event': ('context.' + e.type),
'gtm.element': (e.target)
});
});
});
});
function attachPendingStateClearTimer(dataLayer){
setInterval(function(){
pendingStateClear(dataLayer);
}, 1000);
}
// Install the dataLayer hook to inject properties on all suitable events.
(function(dl) {
var _push = dl.push;
var _slice = Array.prototype.slice;
observer.dataLayer = dl;
attachPendingStateClearTimer(dl);
dl.push = function() {
var result = _slice.call(arguments, 0).map(interceptor);
if(debug) result.map(function(n){ console.log('dl:', n) });
return _push.apply(dl, result);
};
}(window[dataLayer_name] = window[dataLayer_name] || []));
}(window.jQuery));
/* IFRAME cross communication for DataLayer integration. */
(function(window, document, jQuery){
var dataLayerName = 'dataLayer';
var _slice = Array.prototype.slice;
var _filter = Array.prototype.filter;
var _hasProp = Object.prototype.hasOwnProperty;
var _listeners = []; // functions receiving message events
var DEBUG = 0;
var policy = {
// should events received on the datalayer of child frames be copied
// up to the parent?
events_dispatch_up: true,
// should events received on the datalayer of the parent be copied down
// to the child frames?
events_dispatch_down: true
// NOTE: if both the above flags are `true`, child and parent dataLayer
// instances will be kept "in sync" (for all new events loaded after
// the framework loads.)
};
function assert(value, message) {
if (!value) {
throw new Error(message);
}
}
assert(_slice && _filter && window.postMessage && window.JSON,
"Cross frame communication requires a newer browser.");
function _getDataLayer(name){
return (window[name] = window[name] || []);
}
function _insertDatLayerCallback(name, callback){
var dl = _getDataLayer(name);
var _attached = (callback.attached = callback.attached || []);
var _push = dl.push;
if(_attached.indexOf(name) < 0){
dl.push = function(){
_slice.call(arguments, 0).map(callback);
return _push.apply(dl, arguments);
};
_attached.push(name);
}
}
function _event(name, element, props) {
var dataLayer = _getDataLayer(dataLayerName);
var eventObject = {
'event': name,
'gtm.element': element
};
var k = null;
if (props)
for (k in props) {
if (_hasProp.call(props, k)) {
eventObject[k] = props[k];
}
}
return dataLayer.push(eventObject);
}
// Simplified means of attaching event handlers...
function attach(nodes, event, handler, capture) {
var _nodes = (nodes && nodes.length) ? nodes : [nodes];
var _listen = (nodes[0].addEventListener || nodes[0].attachEvent);
_nodes.map(function(n) {
event.replace(/(on)?([a-z]+)/ig, function($0, _on, _event) {
_listen.call(n, _event, handler, capture);
});
});
}
// Install single global listener for the message bus.
attach([window], 'message', function(e) {
_listeners.map(function(callback) {
callback.call(window, e);
});
});
function cloneEvent(e){
return e; // nevermind, JSON handling within the browser will assure its safe.
}
// Within the parent (window), handler for interaction with child IFRAMEs
function attachChildClients(callback) {
var frames = window.frames;
var clients = [];
_slice.call(frames, 0).map(function(item) {
if ((item === window) || !(item.frameElement)) return false;
var frm = item.frameElement;
var targetWindow = frm.contentWindow;
var client = { state: { active: false } };
var name = (frm.getAttribute('name') || frm.getAttribute('id'));
function send(message, mark) {
if(typeof message != 'string')
message = JSON.stringify(cloneEvent(message));
return targetWindow.postMessage(message, targetWindow.location.href);
}
client.frame = frm;
client.send = send;
client.url = function(){ return targetWindow.location.href; };
client.name = name;
_listeners.push(function(e) {
if (e.source && (e.source === targetWindow)) {
if(DEBUG > 2) console.debug('parent received', e);
switch (e.data) {
case 'syn':
return send('synack');
case 'ack':
client.state.active = true;
return callback.call(client, 'init', client.state.active);
default:
return callback.call(client, e.data, client.state.active);
}
}
});
return clients.push(client);
});
return clients;
}
// Within a child (iframe), handler for interaction with parent.
function attachParentClient(callback) {
var parent = window.parent;
var parent_url = document.referrer;
if (parent === window) return null; // this is the root.
function send(message) {
if(typeof message != 'string')
message = JSON.stringify(cloneEvent(message));
return parent.postMessage(message, parent_url);
}
var client = {
state: { acknowledged: 0 },
frame: parent,
send: send,
url: parent_url,
name: null
};
_listeners.push(function(e) {
if(DEBUG > 2) console.debug('child received', e);
if (e.source && (e.source === parent)) {
switch (e.data) {
case 'synack':
client.state.acknowledged++;
send('ack')
return callback.call(client, 'init', client.state.acknowledged);
default:
return callback.call(client, e.data, client.state.acknowledged);
}
}
});
// notify parent of my existence...
client.interval = setInterval(function() {
if (!(client.state.acknowledged)) {
send('syn');
clearInterval(client.interval);
}
}, 500);
return client;
}
function isJSON(text){
// VERY loose qualifier of JSON object strings.
return (text.charAt(0) == '{') && (text.charAt(text.length - 1) == '}');
}
function pushJSONEvent(text, mark){
if(isJSON(text)){
var data = JSON.parse(text);
// Prevent echoing events we've already seen.
if(mark && data[mark]) return false;
return _getDataLayer(dataLayerName).push(data);
}
}
function getDataContext(elem){
var nodes = jQuery && jQuery(elem);
return nodes && nodes.dataContext && nodes.dataContext();
}
function onready(callback){
if(onready.ready) callback.call(document);
else (onready.queue = onready.queue || []).push(callback);
}
(function(m){
function _trigger(){
m.ready = true;
m.queue.map(m);
m.queue.length = 0;
}
if(document.readyState != 'loading'){
_trigger();
} else {
attach([window], 'load', _trigger);
// attach([document], 'DOMContentLoaded', _trigger);
}
}(onready));
var _export = (window.iframeRelay = window.iframeRelay || {});
_export.listeners = _listeners;
onready(function(){
var now = (new Date()).getTime();
var mark = ('$cfr_' + Math.floor(Math.random() * 1E10) + now);
// Parent should...
var children = attachChildClients(function(message) {
var element = this.frame;
if(message.indexOf(mark) > 0) return false;
switch (message) {
case 'init':
var passback = {
'event': 'iframeContextData',
'dataContext': getDataContext(element)
};
passback[mark] = true;
this.send(passback);
return _event('dom.iframeLoaded', element, {
'iframeName': this.name,
'iframeURL': this.url()
});
default:
// Assume that unhandled messages should reach the dataLayer only.
if(policy.events_dispatch_up) pushJSONEvent(message, mark);
}
});
// Child should...
var parent = attachParentClient(function(message) {
if(message.indexOf(mark) > 0) return false;
switch(message){
case 'init':
break;
default:
// Assume that unhandled messages should reach the dataLayer only.
if(policy.events_dispatch_down) pushJSONEvent(message, mark);
}
});
if(policy.events_dispatch_down && children.length){
_insertDatLayerCallback(dataLayerName, function(e){
// Mark the event as "Seen", so we can ignore it if it rebounds.
e[mark] = true;
children.map(function(c){
c.send(e);
});
});
}
if(policy.events_dispatch_up && parent){
_insertDatLayerCallback(dataLayerName, function(e){
e[mark] = true;
parent.send(e);
});
}
_export.parent = parent;
_export.children = children;
_export.mark = mark;
});
}(window, document, window.jQuery));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment