Analytics

Google Tag Manager for A/B Testing: Full Setup Guide

Google Tag Manager A/B testing setup: custom event trigger, data layer variables, the GA4 event tag, Preview mode and the timing bug that skews results.

Flat illustration of a rounded container dispatching small geometric tokens along branching circuit lines into a receiving dashboard panel

Google Tag Manager does not run your A/B test, it carries the evidence of it. The split, the assignment and the statistics belong to the testing tool; the container’s only job is to take the moment a visitor saw a variation and deliver it to Analytics with the right two parameters attached. This guide is a child of the complete guide to GA4 and A/B testing and covers the setup end to end: the custom event trigger, the two data layer variables, the GA4 event tag, the Preview check and the timing bug that quietly ruins the reading.

If you have not designed the impression event yet, start with how to track A/B test events in GA4, which covers the event design and the custom dimensions that make the parameters reportable. This article assumes that design exists and shows how to wire it through a container.

Why a tag manager sits between the test and Analytics

Without a container, the testing snippet has to call the Analytics library itself, which means the parameter names, the event name and the measurement ID all live in application code. Changing any of them is a deploy. With a container, the snippet publishes a single fact to the data layer, “this visitor saw variation B of this experiment”, and the container decides where that fact goes.

That separation is worth something concrete: the analytics team can add a second destination, rename a parameter or fix a wrong measurement ID without touching the code that renders the variation. It also has a cost that is easy to underestimate: the container is a second system with its own publish step, its own versioning and its own ways of failing silently.

Concern Belongs to the testing tool Belongs to the container
Deciding who sees which variation Yes No
Applying the variation to the page Yes No
Calculating significance Yes No
Announcing that an exposure happened Yes, via a data layer push No
Reading the announcement and shaping the event No Yes
Sending it to Analytics and any other destination No Yes

The line to remember: the container must never be the thing that decides anything about the experiment. The moment a container starts choosing variations, you have an experiment whose assignment logic lives in a system with no concept of randomization, no persistence guarantee and no significance model.

Step 1: the custom event trigger

Your snippet pushes the impression at the end of the function that applies the variation:

window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  event: 'experiment_impression',
  experiment_name: 'hero_cta_test',
  variation_name: 'B'
});

In the container, create a trigger of type Custom Event with the event name experiment_impression, matching exactly, no regular expression. Leave it firing on all custom events of that name; the filtering by experiment happens in the report, not in the trigger.

Two details that decide whether this works in production:

Step 2: the two data layer variables

The trigger tells the container that an exposure happened. The variables tell it what the exposure was. Create two, both of type Data Layer Variable:

Variable name Data layer key Example value
DLV experiment name experiment_name hero_cta_test
DLV variation name variation_name B

Set a default value on both, something like not_set, rather than leaving them empty. An empty value in Analytics is indistinguishable from a parameter that was never sent, while an explicit not_set row in the report is a visible symptom that points straight at the snippet.

Resist the temptation to replace these with a custom JavaScript variable that inspects the DOM to work out which variation is on screen. It reads as clever and it is fragile: the day someone renames a class, the variable returns nothing, the tag still fires, and the report fills with blanks that nobody notices until the test is over.

How the three container pieces connect the snippet to AnalyticsThe testing snippet applies the variation and pushes an impression event to the data layer. A custom event trigger in the container listens for that event name. Two data layer variables read the experiment name and the variation name from the same push. A GA4 event tag combines the trigger and the two variables and sends the event to the Analytics property, where the two parameters must already be registered as custom dimensions to be reportable.snippet applies variationthen pushes the eventtrigger: custom eventexperiment impressiontwo data layer variablesexperiment and variationtag: analytics eventwith both parameterscustom dimensionsregistered in AnalyticsBreak any one link and the failure is silent: the tag still fires, the report still loads, and thevariation column fills with blanks that look like low traffic rather than a broken setup.
Four pieces, one contract. The snippet promises to publish two keys; the container promises to read exactly those keys.

Step 3: the Analytics event tag

Create a tag of type GA4 Event, with the measurement ID of the property you actually report on, an event name of experiment_impression, and two event parameters:

Parameter name Value
experiment_name the DLV experiment name variable
variation_name the DLV variation name variable

Attach the custom event trigger from step 1 and nothing else. Then, in Analytics, register both parameters as event-scoped custom dimensions, because a parameter that is not registered is collected and never appears in a report. Registration is not retroactive, so do it before the test starts rather than on the day you first look at the numbers.

Step 4: naming that survives its author

Naming is not cosmetic here, it is what makes the report filterable a year later and what stops two experiments from colliding.

Step 5: proving it works in Preview mode

Preview mode is the only step people skip and the only one that catches the errors that matter. Enter Preview, load the page in the connected tab, and check four things in order:

  1. The event appears in the left hand event stream, with the exact name experiment_impression. If it does not, the push never happened or it happened before the container loaded.
  2. The tag shows under “Tags Fired” for that event, not under “Tags Not Fired”. A tag under the wrong heading means the trigger condition does not match, usually a typo or a leftover exception.
  3. The two variables carry real values, not the default not_set and not blanks. Check the Variables tab for the selected event, not for the page load.
  4. The event appears once, not once per page view and not twice. Two identical impressions from the same visitor means either a duplicated tag or a push that is not guarded by a first-assignment flag.

Only then publish the container. A working Preview and an unpublished container is the single most common reason a correct setup produces an empty report.

Checklist order inside Preview mode before publishing the containerFour checks in sequence. First the impression event appears in the event stream. Second the Analytics tag appears under tags fired rather than tags not fired. Third both data layer variables carry real values instead of the default. Fourth the event appears exactly once per visitor. Only after all four does the container get published.1. event appearsin the stream2. tag undertags fired3. variables carryreal values4. fires exactlyonceEach check catches a different failure: a missing push, a mismatched trigger, a broken variable contract,and a duplicated denominator. None of the four is visible in the report once the test is running.Fifth step, outside Preview: publish the container. A perfect Preview on an unpublished containeris the most common cause of an empty report on day three of the test.
Preview mode is the only place where the setup can fail loudly. After publication, every remaining failure is silent.

The timing bug: firing before the variation renders

This is the mistake worth reading twice, because it produces a plausible looking result that is wrong in a specific direction.

If the impression fires on a generic page load trigger while the variation is applied a few hundred milliseconds later, then every visitor who leaves inside that gap is counted in the denominator without ever seeing the change. Bounces inside that window are not distributed evenly: the variation branch usually does more work, so it loses more people there, so its denominator inflates while its numerator does not. The measured rate of the variation falls, and a neutral test reads as a loss.

The fix is structural rather than statistical. Push the impression from the same code path that finishes applying the variation, so the event cannot exist before the change is on screen. If the variation is applied asynchronously, push after the callback resolves, not after the request is sent. And if your testing tool exposes its own “variation applied” callback, use it as the source of the push rather than reimplementing the detection.

A related check worth running once the test is live: compare the exposed users on both sides. A split that should be 50/50 and is not is a sample ratio mismatch, and it invalidates the reading regardless of how good the p-value looks. The common A/B testing mistakes guide covers how to test that formally.

A worked example, with the numbers that reached Analytics through the container

Suppose the experiment hero_cta_test ran with the event configured exactly as described, and the Exploration report, fed by the tag above, returned these rows grouped by the variation dimension: control (A) had 10,000 users with the experiment_impression event and 500 conversions; variation B had 10,000 users and 575 conversions.

With a p-value of 0.0187, below the 0.05 threshold, and the whole confidence interval above zero, the result is statistically significant: variation B won with a relative improvement of 15%. That verdict is only trustworthy, however, if the audit in the previous sections passed, which is to say if the event really fired after the variation was applied, with no timing bug inflating one of the two denominators. Check the same arithmetic in the calculator below by pasting the numbers from your own report:

Statistical significance calculator
Control (A)
Variation (B)
Control (A) · Rate-
Variation (B) · Rate-
Relative lift-
p-value-
95% CI of the difference-

Two-sided two-proportion z-test. "Not significant" almost always means not enough sample, not that the versions are equal.

Notice how narrow the margin of safety is. The interval starts at +0.125 percentage points, which on a 5% baseline is a relative gain of two and a half percent, not fifteen. A timing bug that inflates the control denominator by even a couple of hundred visitors would move this result across the threshold in either direction, which is why the plumbing deserves as much scrutiny as the statistics.

Make this automatic with Donnu

Everything this guide covered is real tracking engineering: designing the event, wiring a trigger and two variables, building the tag, proving it in Preview, publishing the container, and avoiding a timing bug that shows up nowhere in the report. A mistake in any one of those pieces can mean weeks of contaminated data with no visible symptom.

Donnu removes the root cause rather than the symptom. The split and the record of which variation each visitor saw happen inside the tool itself, at the moment of assignment, without depending on a hand configured trigger or a tag pointing at the right variables. The significance calculation comes out ready as well, with no Exploration report to assemble and no numbers to paste into a separate calculator. Start a free trial and stop depending on a chain of manually configured pieces to know which variation is winning.


Read also: GA4 and A/B Testing: The Complete Integration Guide · How to Track A/B Test Events in GA4 · A/B Testing Statistical Significance · Leia em português

References

Frequently asked questions

Do I need Google Tag Manager to run an A/B test?
No. A tag manager transports the data, it does not run the experiment. The split, the assignment and the statistics live in your testing tool. What the container adds is a place to change what gets sent to Analytics without a code deploy, and a Preview mode that lets you watch the event fire before anything reaches the live property. If your testing tool already writes to Analytics directly, adding a container in the middle buys you flexibility at the cost of one more system that can be misconfigured.
Should the variation be a data layer variable or a custom JavaScript variable?
A data layer variable, in almost every case. It reads a value that your testing snippet explicitly published, at a moment the snippet controls, which makes the contract between the two systems visible and testable. A custom JavaScript variable that scrapes the page, reads a cookie or inspects the DOM to guess which variation is showing looks convenient and fails silently the day the markup changes, usually producing a blank value rather than an error.
Why does my tag fire in Preview mode but nothing appears in GA4?
Three usual causes, in order of frequency. First, the container was never published, so Preview mode works and the live site is unchanged. Second, the event parameters were sent but never registered as custom dimensions in Analytics, so the data is collected and not reportable. Third, the measurement ID in the tag points to a different property than the one you are looking at. Preview mode proves the container works; it proves nothing about the destination.
Can the same container run tags for two experiments at once?
Yes, and that is exactly why the experiment identifier belongs in the event as a parameter rather than being implied by the tag. One trigger, one tag and two parameters handle any number of concurrent experiments, because the report filters by experiment name. Creating one trigger and one tag per experiment works for a while and then becomes a container nobody dares to clean up.
What is the most common mistake in a tag manager A/B testing setup?
Firing the impression event before the variation has been applied to the page. If the trigger listens to a generic page load event while the variation renders a few hundred milliseconds later, every visitor who leaves inside that window is counted as exposed to something they never saw. The bias is asymmetric, because the variation branch usually carries more work and therefore accumulates more phantom exposures, which inflates its denominator and lowers its measured conversion rate.
Does the container slow down the page enough to affect the test?
It can, and if it does it is a validity threat rather than a performance footnote. The container script is an extra request that competes with the rendering of the variation, and any delay that lands on one branch and not the other becomes part of what you are measuring. Keep the container lean, load the testing snippet before the container rather than through it, and if you must choose, protect the render path over the reporting path.