16 Mar 2016
Tracking AJAX events with Google Tag Manager
Reading time: 14 mins.
|
Difficulty:

Tracking AJAX events with Google Tag Manager

Tracking AJAX events with Google Tag Manager

Not that AJAX
No, not that Ajax. Though they rocked in the 90s.

Sometimes we find ourselves needing to measure website content that has been dynamically loaded, or actions related to the loading process.

Normally, this type of content is served via the use of AJAX technology. Without getting too bogged down in theory, it is a functionality that is present in all modern browsers and uses Javascript to enable us to send requests to the web server (or other external servers), receive and process the responses to these requests and use them to create or update content in real time, all without the need to reload the page. Here are some typical examples of where we find them:

  • Pages with “infinite scroll” or automatic pagination without page reload
  • Enlarged photos in a gallery
  • Slides, carousel elements and banners that are updated dynamically
  • Modal windows, information pop-ups, general confirmation/error/etc. messages, content boxes that load after certain actions
  • Data that is calculated or updated in real time without reloading the page (e.g. shopping carts, search bars with auto-complete, etc.)
  • Widgets with data obtained externally (e.g. weather, statistics, etc.)
  • Interactive web apps that require server requests (e.g. online chats, surveys, etc.)

The problem we often come across is that this content is difficult to measure. It’s not present when the page is loaded (and doesn’t appear in the page’s “view source code”), and it’s hard to detect the precise moment at which it’s loaded, in order to know when to update our information and take action as a result – at least, not with just Google Tag Manager’s native events (clicks, form submissions, history changes, etc.).

Detecting AJAX events with Google Tag Manager (with jQuery)

Not this Ajax
Also known as “Francis events”.

In order to be able to measure this content, we need to create a Google Tag Manager tag that will act as an event processor. In other words, it will be present on the current page, detecting the corresponding actions and generating a custom GTM event for each of them, which we can then use as a trigger for other tags.

1. Requirements and restrictions

To achieve this, we’ll use the event controllers that are included in the jQuery library with its AJAX functions. Consequently, we’ll have two restrictions:

  • jQuery (any version is fine) must be installed and included in all of the pages to be measured.
  • It will only be compatible with AJAX calls made via jQuery ($.ajax, .load() and similar). AJAX requests made via pure Javascript or using other methods will not be detected.

If these restrictions present a problem, we offer an alternative solution below 🙂 But for the time being, keep reading!

2. Auxiliary variables

As well as detecting AJAX calls, the event processor will return additional data associated with said calls along with its response, which we can then use in order to distinguish them from each other, send extra information, etc. To do this, we will use generic dataLayer variables that are associated with the custom event and sent in the same push: e.g. eventCategory, eventAction, etc.

Therefore, in order to gather this data and use it in tags and triggers, the first thing we need to do is create the associated Google Tag Manager variables, of the Data Layer Variable type:

variable event category
variable event action
variable event label

Given that they use a fairly standard nomenclature, it’s very likely that you’ve already created them 😉

3. Event processor tag

Now we can create the new tag; specifically, a Custom HTML tag, to which we attach the following code:

<script>
(function($) {
	$(document).ajaxSuccess(function( event, xhr, settings ) {
		dataLayer.push({
			'event': 'ajaxSuccess',
			'eventCategory': 'AJAX',
			'eventAction': settings.url,
			'eventLabel': xhr.responseText
		});
	});
})(jQuery);
</script>

As a trigger, we’ll want it present on each page load. Although in the majority of cases it will work correctly with the default “All pages” trigger, as a precaution (e.g. against potential problems with jQuery yet to be loaded, etc.) we’ll delay it until after the page structure is loaded; i.e. the event is DOM Ready or gtm.dom. To do this, we need to create the corresponding trigger, as follows:

activador DOM

And we’ll set it for the tag we’re creating, so that it ends up like this:

procesador eventos AJAX

Of course, if the AJAX calls to be measured only occur on certain pages or sections of the website, we can take the opportunity to introduce additional conditions (by using variables such as {{Page URL}} or {{Page Path}}) in order to restrict triggering to these areas and thereby optimize performance.

Result and applications

Once the tag has been saved and published, from now on we’ll see a new custom event in our GTM container (we can check it out in the Preview and Debug window) called ajaxSuccess, with the following associated data (data layer variables):

  • event: ajaxSuccess
  • eventCategory: AJAX
  • eventAction: the URL to which the request has been sent. Usually the address of an internal script on the website.
  • eventLabel: the complete response received. Normally it’s the HTML content to be loaded, although it can also be in other formats (JSON, plain text, etc.) or simply an OK/Error-type response.

ejemplo debug ajax popup

From now on, we can use the ajaxSuccess custom event as a trigger for other tags (e.g. Google Analytics events, marketing pixels, additional Javascript code, etc.), along with their associated data (i.e. URL or response) in order to, on the one hand, distinguish and identify specific requests (and thereby only measure these actions or trigger specific tags associated with them), and on the other, collect relevant dynamic information to be sent in said tags (e.g. Analytics event parameters).

Case study: measuring AJAX calls as Google Analytics events

As a first example, simplistic and a little crude yet potentially useful as a testing or debugging operation, let’s measure all AJAX calls and send them as events to a Google Analytics property.

To do this, we need to create a trigger that will fire on all instances of the ajaxSuccess custom event:

activador AJAX

That done, we now need to create an Event-type Google Analytics tag, fill in the ID of our property and other desired parameters and use dynamic data for the event parameters. For example:

etiqueta evento AJAX

Note that we’re not using the {{Event Label}} variable directly; this is not recommended, as it gives us the complete response, which could be an extremely long, unpredictable or illegible dataset or output in an unknown format. Unless we’re 100% sure of what’s going to be there, it’s not a good idea to send it to Analytics as-is. We could create some kind of intermediate variable to process the data and extract something specific (e.g. the name of an alert or the URL of an image), but that’s left as an exercise to the reader. 😉

Of course, although this may be of use to us in a simple example, normally we won’t be interested in measuring all AJAX requests indiscriminately. The usual aim will be to detect certain specific operations and use the associated data to identify them. Let’s look at a couple of examples.

Case study: tracking the opening of pop-ups or modal windows

este ajax menos, truño de peli
PLEASE SUBSCRIBE TO MY NEWSLETTER!

Here’s a more specific hypothetical case. Let’s imagine we have a website at http://www.anawesomecompany.com, and at certain moments it loads and presents the user with modal windows, lightboxes and/or (false) pop-ups, i.e. temporary content boxes that are superimposed onto the page in order to attract the user’s attention, offer them information, alert them to an incident or ask them to perform an action. We want to track the apparition of these windows and boxes in Analytics.

For a start, there’s no easy way to measure it using our usual GTM events, as they can be generated for many different reasons: clicking on certain links or buttons, validation errors, opening certain content… maybe we even have one appear automatically every once in a while and ask us to subscribe to a newsletter, because we’re just that annoying.

But after a little research, we’ve noticed they all have something in common: their content is dynamically loaded in real time by AJAX. We can use this to detect when they’re opened.

Luckily, as we developed the website ourselves, or someone we trust completely has documented it all and is happy for us to nag them whenever we like, we won’t waste any time in identifying the exact structure of the requests to – hahahaha, OK, OK, let’s get back to the real world. We’ll need to investigate a little.

First of all, and supposing that the corresponding Google Tag Manager container is already installed and functioning on the website, we need to install the AJAX event processor mentioned earlier and activate Preview and Debug mode.

After browsing for a while we’ll see the ajaxSuccess events appear. If we click on them, in the Data Layer tab we can see the data they’re sent with, and from there we can look for something common to each case that we can measure.

ejemplo debug ajax popup

For this example, let’s imagine that each opening of a pop-up is accompanied by a call to a URL with the same structure: “/get_popup?id_popup=XXXX”. As this is captured in the {{Event Action}} variable, we’ll use it to distinguish these events from other AJAX calls.

So we create a new trigger, give it a name (e.g. AJAX Popup) and assign parameters:

  • Type: Custom event
  • Event name: ajaxSuccess
  • Additional condition: {{Event Action}} contains /get_popup

activador AJAX popup

Bingo! Now we have a way to detect them.

Now, if we want to measure all these popups as Google Analytics events, we can (for example) create a tag such as the one below and associate it with the trigger:

etiqueta evento popup

Or perhaps we want to go a step further and count each pop-up as a separate page load, therefore sending virtual pages to Analytics.

Let’s use the AJAX request itself as a virtual URL (i.e. the page parameter of the Analytics tag) directly, as it’s normally sufficient to identify each specific case.We already have it in {{Event Action}}, but as it’s an absolute URL (http://www.anawesomecompany.com/stuff) and normally only the path or relative URL (/stuff) is sent to Analytics, we’ll need to create an intermediate auxiliary variable that will extract the pathname from the data:

variable event action path

Now we can send it as a virtual URL in a Page View-type Analytics tag:

etiqueta pageview popup

Notes for nerds: See how we’ve also manually set the Referrer parameter, in this instance with the current URL, which will be then identified as the original source of the call to the virtual page. We recommend doing it this way because, if we don’t overwrite it, the hit will use the referrer of the current page by default. Which, as well as being technically less correct, could also cause problems if it’s external, as it might break/restart the session, etc. But that’s a debate for another day 😉

Case study: virtual confirmation pages

Now that we’ve seen how to detect specific loading of AJAX content and how to use it to send virtual pages in Analytics, we’re able to simulate page loads wherever one is needed.

For example, if we have a contact form which, once completed successfully, doesn’t send/redirect us to a confirmation or “Thank you” page but instead sends the form via AJAX, remaining on the page and displaying an “OK” message, we can use this operation to generate a virtual confirmation page which we can then configure in Analytics for Goals, conversion funnels, etc.

The steps are the same as for the previous example. We create the processor, activate it in Preview and run tests to examine the data layers generated.

In this case, we may encounter an additional problem: all the requests are sent to the same URL, whether failed attempts (which return validation errors, delivery errors, etc.) or correct attempts (which return the confirmation message).

Given that we’re only interested in measuring the latter, we need an additional way to tell them apart.

Example: success
ejemplo debug ajax contacto OK

Example: failure
ejemplo debug ajax contacto KO

Luckily, as the processor also returns the response received in another parameter, we can use this to distinguish the two. We need to look for something that is common to all the instances we want to measure, but is not present in the others: in this case, the div class=“contact-response-ok”. We then add it as a condition to the corresponding trigger:

  • Type: Custom event
  • Event name: ajaxSuccess
  • Additional condition: {{Event Action}} contains /contact/send.php
  • Additional condition: {{Event Label}} contains contact-response-ok

activador contacto OK

Now all we need to do is create the corresponding tag and associate it with the new trigger:

etiqueta pageview contacto OK

Detecting AJAX events with Google Tag Manager WITHOUT jQuery

What if our website doesn’t have jQuery? Well, we could include it via GTM, but in addition to being a fairly inefficient solution if we don’t need it for anything else, it doesn’t solve anything if the AJAX requests we want to measure are made via a library other than jQuery, or directly via pure Javascript (XMLHttpRequest).

Luckily, if you have this problem, we have a solution for you 🙂

When creating the event processor tag, use the following code instead (idea taken from this discussion on the all-powerful and inexhaustible stackoverflow):

<script>
(function() {
	var xhrSend = window.XMLHttpRequest.prototype.send;
	window.XMLHttpRequest.prototype.send = function() {
		var xhr = this;
		var intervalId = window.setInterval(function() {
			if(xhr.readyState != 4) {
				return;
			}
			dataLayer.push({
				'event': 'ajaxSuccess',
				'eventCategory': 'AJAX',
				'eventAction': xhr.responseURL,
				'eventLabel': xhr.responseText
			});
			clearInterval(intervalId);
		}, 1);
		return xhrSend.apply(this, [].slice.call(arguments));
	};
})();
</script>

Notes for nerds:

  • The script directly patches the native XMLHttpRequest.send() function, so it’ll be compatible with any browser that uses it and any script or library whose AJAX functionality depends on it.
  • For greater compatibility, rather than depend on the OnStateChange property of the request, it directly monitors its status, which makes it compatible with almost all AJAX requests – including those made by jQuery (which wouldn’t otherwise work as they bypass OnStateChange and use their own callbacks instead).
  • If you have any problems, try increasing the wait time for the setTimeout call.

Bonus track: capturing additional data

In certain advanced cases, the data we’ve captured up to now may not be sufficient to give us everything we need. The most common issue is that we need something from the parameters we sent to the AJAX call but can’t see them, because rather than being embedded within the requested URL they’re sent separately in a POST-type request.

For cases such as these, I’ll give you a slightly more complex version of both event processors (i.e. with and without jQuery) that captures additional data. Use, share and modify it however you like 😀

  • event: ajaxSuccess
  • eventCategory: AJAX GET, AJAX POST… as per HTTP request type
  • eventAction: the URL to which the request has been sent, but INCLUDING all additional data/parameters (separated by a semicolon “;”) that we wouldn’t normally see if it was a POST request
  • eventLabel: the response received

With jQuery:

<script>
(function($) {
	$(document).ajaxSuccess(function( event, xhr, settings ) {
		dataLayer.push({
			'event': 'ajaxSuccess',
			'eventCategory': 'AJAX ' + settings.type,
			'eventAction': settings.url + (settings.type == 'POST' && settings.data ? ';' + settings.data : ''),
			'eventLabel': xhr.responseText
		});
	});
})(jQuery);
</script>

Without jQuery:

<script>
(function() {
	var xhrOpen = window.XMLHttpRequest.prototype.open;
	var xhrSend = window.XMLHttpRequest.prototype.send;
	window.XMLHttpRequest.prototype.open = function() {
		this.method = arguments[0];
		this.url = arguments[1];
		return xhrOpen.apply(this, [].slice.call(arguments));
	};
	window.XMLHttpRequest.prototype.send = function() {
		var xhr = this;
		var xhrData = arguments[0];
		var intervalId = window.setInterval(function() {
			if(xhr.readyState != 4) {
				return;
			}
			dataLayer.push({
				'event': 'ajaxSuccess',
				'eventCategory': 'AJAX ' + xhr.method,
				'eventAction': xhr.url + (xhr.method == 'POST' && xhrData ? ';' + xhrData : ''),
				'eventLabel': xhr.responseText
			});
			clearInterval(intervalId);
		}, 1);
		return xhrSend.apply(this, [].slice.call(arguments));
	};
})();
</script>

If, after that, you still need help, it might be a good time to get in touch with us. We can help you! 😉