Free AJAX Event Listener For Google Tag Manager

August 27, 2015
Free AJAX Event Listener For Google Tag Manager

Our blog posts are frequently borne out of the experiences we have working with clients or on our own website. Often we’ll create a tool and think to ourselves, could others benefit from this? We created this AJAX listener for Google Tag Manager to help monitor completed requests and then fire tags appropriately, and we definitely think others will have a need for it. Give it a shot and let us know how you’re using it!

jQuery, one of the most ubiquitous JavaScript libraries in the world, offers several easy to use methods for performing AJAX requests. It also enhances these requests by emitting globally scoped events when the request starts and finishes. We’ve created a simple Custom HTML Tag that binds to these events, so we can push data about them into the Data Layer. This can be useful for triggering tags after asynchronous resources have been resolved, retrieving query parameters or headers from on-page requests, and more.

The tag will listen for $.ajax and all of its kin, including $.post, $.getJSON, $.getScript, et. al. When an AJAX request is completed, it will push details about that request into the Data Layer; here’s an example push:

  "event": "ajaxComplete",
  "attributes": {
    "type": "GET",
    "url": "",
    "queryParameters": {},
    "pathname": "/json/",
    "hostname": "",
    "protocol": "https:",
    "fragment": "",
    "statusCode": 200,
    "statusText": "OK",
    "headers": {
      "content-type": "application/json"
    "timestamp": 1458242558005,
    "contentType": "application/x-www-form-urlencoded; charset=UTF-8",
    "response": {
      "ip": "",
      "country_code": "US",
      "country_name": "United States",
      "region_code": "PA",
      "region_name": "Pennsylvania",
      "city": "Pittsburgh",
      "zip_code": "15222",
      "time_zone": "America/New_York",
      "latitude": 40.4495,
      "longitude": -79.988,
      "metro_code": 508

The Code

This code is available freely under the MIT License. We make no guarantees of performance or reliability, and you use this at your own risk. If you do happen to see a bug or a place for improvement, please let us know in the comments.

<script id="gtm-jq-ajax-listen" type="text/javascript">
  (function() {
    'use strict';
	var $;
    var n = 0;
    function init(n) {
      // Ensure jQuery is available before anything
      if (typeof jQuery !== 'undefined') {
        // Define our $ shortcut locally
        $ = jQuery;
      // Check for up to 10 seconds
      } else if (n < 20) {
        setTimeout(init, 500);
    function bindToAjax() {
      $(document).bind('ajaxComplete', function(evt, jqXhr, opts) {
        // Create a fake a element for magically simple URL parsing
        var fullUrl = document.createElement('a');
        fullUrl.href = opts.url;
        // IE9+ strips the leading slash from a.pathname because who wants to get home on time Friday anyways
        var pathname = fullUrl.pathname[0] === '/' ? fullUrl.pathname : '/' + fullUrl.pathname;
        // Manually remove the leading question mark, if there is one
        var queryString =[0] === '?' ? :;
        // Turn our params and headers into objects for easier reference
        var queryParameters = objMap(queryString, '&', '=', true);
        var headers = objMap(jqXhr.getAllResponseHeaders(), '\n', ':');
        // Blindly push to the dataLayer because this fires within GTM
          'event': 'ajaxComplete',
          'attributes': {
            // Return empty strings to prevent accidental inheritance of old data
            'type': opts.type || '',
            'url': fullUrl.href || '',
            'queryParameters': queryParameters,
            'pathname': pathname || '',
            'hostname': fullUrl.hostname || '',
            'protocol': fullUrl.protocol || '',
            'fragment': fullUrl.hash || '',
            'statusCode': jqXhr.status || '',
            'statusText': jqXhr.statusText || '',
            'headers': headers,
            'timestamp': evt.timeStamp || '',
            'contentType': opts.contentType || '',
            // Defer to jQuery's handling of the response
            'response': (jqXhr.responseJSON || jqXhr.responseXML || jqXhr.responseText || '')
    function objMap(data, delim, spl, decode) {
      var obj = {};
      // If one of our parameters is missing, return an empty object
      if (!data || !delim || !spl) {
        return {};
      var arr = data.split(delim);
      var i;
      if (arr) {
        for (i = 0; i < arr.length; i++) {
          // If the decode flag is present, URL decode the set
          var item = decode ? decodeURIComponent(arr[i]) : arr[i];
          var pair = item.split(spl);
          var key = trim_(pair[0]);
          var value = trim_(pair[1]);
          if (key && value) {
            obj[key] = value;
      return obj;
    // Basic .trim() polyfill
    function trim_(str) {
      if (str) {
        return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
   * v0.1.0
   * Created by the Google Analytics consultants at
   * Written by @notdanwilkerson
   * Documentation:
   * Licensed under the Creative Commons 4.0 Attribution Public License

Get It For Yourself

You can copy the above code and stick it into a script tag, or simply merge this container file with your Google Tag Manager container to import the tag and Data Layer Variables for all of the data it surfaces with each event. For instructions on how to do this, see Jim's post detailing how to use container import.

Tell Us What You Do With It

If you use this listener, we'd love to hear about your use cases. We've identified a couple in this post, but please don't hesitate to share if you've found another way to put this to work. As always, if you spot a bug, please let us know in the comments and we'll fix it straight away.