How to Filter Bot Traffic in GA4 via Google Tag Manager

Label invalid traffic with Transfon Gateway and exclude it from GA4 reports

Bots and invalid traffic inflate pageviews, wreck conversion rates, and make GA4 reports unreliable. GA4's built-in bot filtering only covers known crawlers on the IAB spiders list. It misses headless browsers, scrapers, and click bots. This guide shows how to load Google Tag Manager through the Transfon Gateway tag so every visitor is labeled with a traffic_type in the dataLayer before GTM loads, and GA4 can filter bots out.

How It Works

  1. Your GTM loader is wrapped in a global window.waf360Load(trafficType) function.
  2. The Transfon Gateway tag loads, evaluates the visitor, and calls waf360Load() with the verdict: bot for flagged visitors, clean for everyone else.
  3. waf360Load() pushes a traffic_type event to the dataLayer, then loads GTM.
  4. Your GA4 tag sends traffic_type with every hit, which GA4 data filters can exclude.

Two failsafes guarantee GTM always loads, even if Transfon Gateway is slow or unreachable: an onerror handler on the tag script and a timeout fallback. In both cases traffic is labeled clean.

Step 1: Install the Tags

Your standard GTM snippet looks like this:

<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXX');</script>
<!-- End Google Tag Manager -->

Replace it with the following. Substitute GTM-XXXXXXX with your container ID and YOUR_LICENSE_KEY with your Transfon Gateway license key.

<!-- Google Tag Manager, loaded via Transfon Gateway -->
<script>
window.waf360Load=function(t){if(waf360Load.d)return;waf360Load.d=1;
window.dataLayer=window.dataLayer||[];
dataLayer.push({event:'traffic_type',traffic_type:t||'clean'});
!function(e,t,a,n){e[n]=e[n]||[],e[n].push({"gtm.start":(new Date).getTime(),event:"gtm.js"}),n=t.getElementsByTagName(a)[0],(a=t.createElement(a)).async=!0,a.src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX",n.parentNode.insertBefore(a,n)}(window,document,"script","dataLayer")
};setTimeout(waf360Load,1000);
</script>
<script id="waf360-tag" src="https://tag.waf360.com/tag/tag.js" data-license="YOUR_LICENSE_KEY" async
        onerror="waf360Load()"></script>
<!-- End Google Tag Manager -->

What each piece does:

  • window.waf360Load(t) wraps the original GTM loader. It pushes a traffic_type event to the dataLayer first (using the value passed by the tag, or clean when called without one) and then loads GTM. The d flag makes it idempotent, so it only ever runs once.
  • The Transfon Gateway tag calls waf360Load(trafficType) automatically when its check finishes, so no _waf360.check() callback or queue stub is needed. Flagged visitors (blocked, or failing the quality check) are labeled bot; everyone else is labeled clean.
  • onerror="waf360Load()": if the Transfon Gateway tag fails to load (network error, blocked), GTM loads immediately.
  • setTimeout(waf360Load, 1000): if the check takes longer than 1 second, GTM loads anyway. Whichever path fires first wins; later calls are no-ops.
  • The <noscript> GTM iframe fallback (if you use it) can stay where it is unchanged.

Note the trade-off in the timeout: if the check responds after 1 second, GTM has already loaded with traffic labeled clean. Raise the timeout if labeling accuracy matters more than analytics latency.

Optional attributes on the tag script:

  • data-sample-rate="0.2": run the verification on a sample of pageviews (here 20%) to reduce tag load on very high-traffic sites. On unsampled pageviews the timeout fallback fires and traffic is labeled clean, so use 1 (or omit the attribute) if you want every hit classified.

Step 2: Create the dataLayer Variable in GTM

  1. In GTM, go to Variables → New → Data Layer Variable.
  2. Set Data Layer Variable Name to traffic_type.
  3. Optionally, under Format Value, set Convert undefined to clean as a safety net (every visitor is already labeled by waf360Load).
  4. Name it DLV - traffic_type and save.

Step 3: Send traffic_type to GA4

  1. Open your Google Tag (GA4 configuration tag).
  2. Under Configuration settings (or Fields to Set in older setups), add:
    • Field name: traffic_type
    • Value: {{DLV - traffic_type}}
  3. Save and publish the container.

Every GA4 hit now carries a traffic_type parameter: bot for flagged visitors, clean otherwise.

Step 4: Create the GA4 Data Filter

  1. In GA4, go to Admin → Data collection and modification → Data filters.
  2. Click Create filter → Internal Traffic.
  3. Set Filter operation to Exclude and Parameter value to bot.
  4. Set the filter state to Testing first. In Testing mode, filtered events are labeled with a Test data filter name dimension instead of being dropped, so you can verify before excluding data permanently.
  5. Once verified, switch the filter state to Active.

Data excluded by an active filter is permanently removed from reports and cannot be backfilled. Always validate in Testing mode first.

Step 5: Verify

  1. Enable GTM Preview mode and load your site, then confirm the traffic_type event appears in the dataLayer timeline and your Google Tag fires after it.
  2. In GA4 DebugView, confirm hits carry the traffic_type parameter.
  3. To simulate bot traffic, visit your site with a headless browser (e.g. curl won't run JS, use Puppeteer or Chrome headless) and confirm traffic_type: bot is set.

Alternative: Block Tags Entirely for Bots

Instead of labeling bot traffic, you can stop GA4 (and any other tag) from firing at all:

  1. Create a Custom Event trigger in GTM with event name traffic_type.
  2. Add a condition: DLV - traffic_type equals bot.
  3. Add this trigger as an Exception (blocking trigger) on your Google Tag.

This saves GA4 quota and third-party tag costs for bot traffic, at the price of losing visibility into how much bot traffic you receive. Labeling (Steps 2–4) is recommended if you want to monitor bot volume in reports; blocking is better if you are billed per event by third-party vendors.

Cleaning Traffic for Other Tags

The same approach works for any tag that isn't managed through GTM, such as a Meta Pixel, ad conversion pixels, an affiliate tracker, a session-recording script, or a gtag.js install without a container. Add the queue stub, then use window._waf360.check() to decide whether to load the tag:

<!-- Queue stub, then the Transfon Gateway tag -->
<script>window._waf360={q:[],send:(w,c)=>window._waf360.q.push([w,c]),check:w=>window._waf360.q.push(["check",w])};</script>
<script id="waf360-tag" src="https://tag.waf360.com/tag/tag.js" data-license="YOUR_LICENSE_KEY" async></script>

<!-- Gate your tags on the verdict -->
<script>
window._waf360.check(function(data) {
  if (data.block || !data.qs) {
    // Flagged traffic: do not load the tag
  } else {
    // Clean traffic: paste your existing tag snippet here, e.g.
    // !function(f,b,e,v,n,t,s){...}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');
    // fbq('init','YOUR_PIXEL_ID'); fbq('track','PageView');
  }
});
</script>
  • window._waf360.check(callback) is answered as soon as the tag has evaluated the visitor. data.block is true for blocked traffic and data.qs is false when the visitor fails the quality check. See the JavaScript API for the full response fields.
  • Because of the queue stub, check() can be called anywhere on the page, even before the tag script has loaded.
  • If the tag script never loads (blocked by an extension, network error), the callback never runs and the tag is not loaded. If you would rather load tags in that case, add onerror and timeout failsafes as shown in Step 1.

For tags that accept a custom parameter, such as gtag.js, you can label instead of block: load the tag in both branches and pass the verdict:

<script>
window._waf360.check(function(data) {
  var t = (data.block || !data.qs) ? 'bot' : 'clean';
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXXXXX', { traffic_type: t });
  // then load https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX
});
</script>

The traffic_type value flows into the same GA4 data filter described in Step 4. For pixels with no equivalent parameter (Meta, most ad networks), blocking is the right choice, since flagged traffic firing conversion pixels is exactly what pollutes ad-platform learning.