For many, it seems, one of the most important justifications for server-side Google Tag Manager is its resilience to ad and content blockers. After all, by virtue of serving the container JavaScript from your own domain, you escape many of the heuristic devices today’s blocker technologies employ.

Well, I’ve gone on record over and over again to say how this is poor justification for using server-side GTM. By circumventing the user’s wish to block scripts, you are disrespecting them and forcing their browser to download scripts that they wanted to avoid downloading in the first place.

However, this is not another article lamenting this disconnect between data-hungry analysts and their unsuspecting site visitors.

Instead, I want to take the technology that allows you to circumvent and instead use it to detect.

While ad and content blockers should be allowed to run their course, I do think it’s absolutely vital that their impact be measured. That way you can calculate just how many of your analytics and ad requests are blocked due to these tools.

You can even use the detection technology to conditionally block your server-side tags! That way you can truly act according to the user’s wishes and keep those data streams between the browser and the server container alive.

By the way, if you’re interested in knowing more about ad and content blockers in general, I recommend you listen to this Technical Marketing Handbook podcast episode, where I interviewed Pete Snyder of Brave about this very topic.

X

The Simmer Newsletter

Subscribe to the Simmer newsletter to get the latest news and content from Simo Ahava into your email inbox!

Measuring blocker impact

Let’s start with a major caveat (first of many):

This is a proof of concept. What I’m showing you here are some of the components you need to create the detection system, but most of the work needs to be done client-side (where the detection happens), so you need to adjust these ideas to make sense for what your site runs on.

In this article, I’ll show you how I’m measuring three different things:

  • The ratio of homepage views where ads were blocked.
  • The ratio of homepage views where Google Analytics was blocked.
  • The ratio of homepage views where Google Tag Manager was blocked.

There are additional caveats related to each of these measurements, but I’ll tackle those when they come up.

Note that to understand everything that’s happening here, you need a fairly good grasp of server-side tagging. If you haven’t set it up yet, I recommend you take a look at my article on the topic.

Custom Client template

I’m using a custom Client template in the Server container to pick up requests for the ad blocker bait file as well as for the pixel that is used to generate an event data object.

The bait file contains instructions for the browser to generate an element with the ID GxsCRdhiJi. This is the element you need to search for to see if the file was blocked or not.

Client-side script

Most of the magic needs to be done client-side.

In the page template of my site, I’m running the following script in the page HTML. Yes, you need to do this outside Google Tag Manager, because if GTM is blocked then you won’t be able to measure the impact due to the client-side script being blocked, too.

(function() {
  // Set these to the endpoints configured in the Client template
  var baitPath = 'https://sgtm.simoahava.com/ads-min.js';
  var pixelPath = 'https://sgtm.simoahava.com/4dchk';
  
  // Prevent the script from running outside the home page
  if (document.location.pathname !== '/') return;
  
  // Inject the bait file
  var el = document.createElement('script');
  el.src = baitPath;
  document.body.appendChild(el);
  
  var gaBlocked = false;
  
  // Run the detections at page load to avoid race conditions
  window.addEventListener('load', function() {
    // Send a HEAD request for the Universal Analytics library to see if it's blocked
    fetch('https://www.google-analytics.com/analytics.js', {method: 'HEAD', mode: 'no-cors'})
      .catch(function() {
        // If the load failed, assume GA was blocked
        gaBlocked = true;
      })
      .finally(function() {
        // Build the GA4 parameters, add additional parameters at your leisure
        var params = {
          ads_blocked: !document.querySelector('#GxsCRdhiJi'), // Detect if the bait file was blocked
          gtm_blocked: !(window.google_tag_manager && window.google_tag_manager.dataLayer), // Detect if gtm.js was blocked
          ga_blocked: gaBlocked // Detect if analytics.js was blocked
        };
        
        // Build the pixel request with a unique, random Client ID
        var cid = Math.floor((Math.random() * 1000000) + 1) + '_' + new Date().getTime();
        var img = document.createElement('img');
        img.style = 'width: 1; height: 1; display: none;';
        img.src = pixelPath + '?client_id=' + cid + '&' + Object.keys(params).reduce(function(acc, cur) { return acc.concat(cur + '=' + params[cur]);}, []).join('&');
        document.body.appendChild(img);
      });
  });
})();

I only want to run this script on the home page, and because I’m so lazy at editing my templates, I simply exit the function if the current page path isn’t the root of the site.

For baitPath and pixelPath, configure the corresponding settings you added to the Client template. The default values are /ads-min.js for the bait file path, and /4dchk for the pixel path.

In the following chapters, I’ll tell you how I’m measuring ad blocker use, Google Analytics blocking, and Google Tag Manager blocking.

Measuring ad blockers

To measure the impact of ad blockers, I’m using an age-old heuristic system to detect if the browser is blocking ads.

The browser sends a request to the Server container, where the Client template picks it up. The request needs to be for a file that is most commonly blocked by ad blockers, such as ads-min.js.

...
var baitPath = 'https://sgtm.simoahava.com/ads-min.js';
var el = document.createElement('script');
el.src = baitPath;
document.body.appendChild(el);
...

The purpose of the file is to create a dummy HTML element on the page with a specific ID (GxsCRdhiJi if you’re using the default template). If the element doesn’t exist when you query it, it means that the download failed for some reason or other, one of which could be due to ad blockers.

Caveat: We don’t know why the download failed. It could be due to an ad blocker, but it could also be due to a network error or something else. So there’s always some uncertainty in this regard.

Note also that filter lists such as Disconnect.me, used in e.g. Mozilla Firefox and Microsoft Edge, pattern match against the domain name, which makes detecting them with a custom solution very difficult to do.

Measuring GA blockers

To measure whether Google Analytics is blocked or not, I’m using a very simple method. I run a fetch() call for the Universal Analytics JavaScript library. If that request fails, I’ll chalk it up to GA being blocked.

The request is sent with the HEAD method to prevent exerting browser resources, and it’s sent with the no-cors mode to avoid CORS issues.

Here’s what the request looks like:

...
fetch('https://www.google-analytics.com/analytics.js', {method: 'HEAD', mode: 'no-cors'})
  .catch(() => {
    // GA is blocked
  });
...

Caveat: Again, we don’t know if it’s a blocker that prevents GA from loading. Also, in e.g. Firefox, even in Private Windows, Google Analytics is allowed to load, but its main methods are shimmed to prevent it from working. This particular approach would not be able to detect that type of blocking.

Measuring GTM blockers

For GTM, we’re taking an even simpler approach. At window loaded time, we’ll check if the window.google_tag_manager interface has been created and that it also has the nested object window.google_tag_manager.dataLayer.

...
gtm_blocked: !(window.google_tag_manager && window.google_tag_manager.dataLayer)
...

Caveat: All the aforementioned caveats apply, again. And it’s important to remember that on e.g. Firefox, the global window.google_tag_manager interface is created, but since dataLayer.push is shimmed, GTM itself won’t work.

Sending the pixel request to the Server container

Once you have your Booleans for whether or not these three vectors are being blocked, it’s time to put everything together.

window.addEventListener('load', function() {
  fetch('https://www.google-analytics.com/analytics.js', {method: 'HEAD', mode: 'no-cors'})
    .catch(function() {
      gaBlocked = true;
    })
    .finally(function() {
      var params = {
        ads_blocked: !document.querySelector('#GxsCRdhiJi'),
        gtm_blocked: !(window.google_tag_manager && window.google_tag_manager.dataLayer),
        ga_blocked: gaBlocked
      };
      var cid = Math.floor((Math.random() * 1000000) + 1) + '_' + new Date().getTime();
      var img = document.createElement('img');
      img.style = 'width: 1; height: 1; display: none;';
      img.src = pixelPath + '?client_id=' + cid + '&' + Object.keys(params).reduce(function(acc, cur) { return acc.concat(cur + '=' + params[cur]);}, []).join('&');
      document.body.appendChild(img);
    });
});

The pixel request waits for the window to completely load and for the Google Analytics fetch() request to complete. This way any possible asynchronous race conditions won’t spoil the measurement.

In the params object, you can add any parameters you wish to end up in the event data object. If you don’t provide the page_location parameter, you’ll need to add it to the tag (see below). You can also provide a custom event_name parameter, and the Client template will default to page_view if you don’t.

The client_id is automatically generated as a unique, random value for each request.

Caveat: This means that we’re not measuring users who use blockers but rather individual hits and whether they’re blocked or not. I wanted to ignore users simply because I don’t want to start setting cookies or fingerprinting just for the purpose of this exercise.

Finally, a pixel request is sent to your server container, which grabs it and creates an Event Data Object with the parameters:

Setup the GA4 tag

In the Server container, you can then configure a GA4 tag to fire when this particular event data object is generated. For this purpose, I recommend you create a new data stream just for this data. That way you don’t pollute your marketing analytics data with this ad blocker coverage.

As you can see, I’m redacting the visitor’s IP address (I consider that to be a required setting in ALL GA4 measurement).

Remember to add your data stream Measurement ID to the tag. You don’t need to add any of the custom parameters from the pixel request – they will automatically be sent to GA4 just by virtue of being in the event data object.

Finally, here’s what the trigger looks like:

In Google Analytics 4, remember to configure the custom parameters as custom dimensions.

Test the setup

Once everything is in place, you can check the Server container preview mode while loading the homepage of your site.

You should see a request for /ads-min.js followed shortly by a request for the /4dchk pixel.

In Google Analytics 4, you should see your events land in the Real Time report.

Note that everything else in GA4 should be pretty much flatlining. As you’re not using the recommended SDKs, you’re also not measuring user activity and engagement correctly. But we don’t care about that – we just care about the events.

You can then compile them into whatever reports you wish using the Explore section of Google Analytics 4 or even Google BigQuery if you’ve configured the export for this data stream.

Remember to also test with a variety of blockers! For example, this is what it looks like in the browser’s network requests when I load the homepage with the Brave browser:

As you can see, all three parameters have the value true, indicating that on Brave, everything is blocked.

In Firefox Private Windows, however:

Nothing is blocked!

Well, I’m using ads-min.js as the bait file, and the filter list that Firefox uses (Disconnect.me) doesn’t match against file names but rather against tracking domains themselves. And while GA and GTM aren’t technically blocked, their main methods are shimmed, meaning they won’t work in Firefox.

Shimming means that the methods exist to avoid pages from breaking due to reference errors, but they are overwritten in a way that prevents them from doing what they were intended to do.

Summary

You might have noticed how many caveats there were listen in the article. That’s because detecting ad and content blockers is hard.

Blockers are designed in a way to make them as unobtrusive as possible, and while they rarely succeed in being transparent, they do make it difficult to detect and adjust in code.

I’m sure there are more sophisticated ways to detect different blocking mechanisms than those I introduced in this article, but the purpose of this article wasn’t to show you how to detect blockers but rather how to measure their impact.

With a Google Tag Manager Server container, you have an inexpensive API endpoint that’s relatively easy to set up. It runs on your own domain, so the ad blocker detector itself isn’t blocked by, well, ad blockers.

It’s a nice use case for a Server container, again.

With this method, you could selectively block your Server tags from firing if the user is using an ad blocker. Of course, this wouldn’t help with the use cases where the user uses blockers to simply prevent any extraneous scripts from firing in their browser. It’s up to you to decide how far you want to take your respect for the user’s wishes.

If you’re curious, in my tests I found the following:

  • ~7% of page loads happened where Google Tag Manager was blocked.
  • ~10% of page loads happened where Google Analytics was blocked.
  • ~18% of page loads happened where ads-min.js and thus, by proxy, ads were blocked.

Please let me know in the comments if you have questions about this setup or comments about ad / content blocking in general.