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.

📚 This article is part of the guide GA4 and A/B Testing: The Complete Integration Guide.
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:
- The push must happen after the container has loaded, or the container will never hear it. If your snippet runs very early, either queue the push until the container is ready or use a trigger that also inspects the existing data layer state on container load.
- Do not reuse a built-in trigger. Page View, DOM Ready and Window Loaded all fire on a schedule that has nothing to do with when your variation rendered, which is the source of the timing bug covered further down.
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.
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.
- Event name: one fixed name for every experiment,
experiment_impression. Do not encode the experiment in the event name; that multiplies events and burns quota. - Experiment name: stable, lowercase, no spaces, describing the surface and the idea, such as
hero_cta_testorcheckout_steps_2026q3. Never a date alone, never a ticket number alone. - Variation name:
Afor control andB,Conward for variations, or descriptive slugs if you prefer, but consistent across every experiment so the reports look the same. - Case: pick one and enforce it. Analytics treats
Bandbas two different dimension values, which silently splits one variation into two rows and halves both of them.
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:
- 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. - 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.
- The two variables carry real values, not the default
not_setand not blanks. Check the Variables tab for the selected event, not for the page load. - 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.
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.
- Rate of A: 500 divided by 10,000 equals 5.00%. Rate of B: 575 divided by 10,000 equals 5.75%.
- Relative improvement: (5.75 minus 5.00) divided by 5.00 equals +15.0%.
- Pooled rate: (500 plus 575) divided by 20,000 equals 5.375%.
- Standard error: the square root of 0.05375 times 0.94625 times (1 divided by 10,000 plus 1 divided by 10,000), about 0.00319.
- z score: (0.0575 minus 0.0500) divided by 0.00319, about 2.35.
- Two sided p-value: about 0.0187.
- 95% confidence interval on the difference: roughly +0.125 to +1.375 percentage points.
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:
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
- Google Tag Manager Help. Data layer. Reference for
dataLayer.push, custom event triggers and data layer variables. developers.google.com/tag-platform/tag-manager/datalayer. - Google Tag Manager Help. Preview and debug containers. How the event stream, the fired and not fired tag lists and the variables tab work. support.google.com/tagmanager/answer/6107056.
- Google Tag Manager Help. Google Analytics: GA4 Event tag. Configuring event names and event parameters. support.google.com/tagmanager/answer/9442095.
- Google Analytics Help. Custom dimensions and metrics. Event scope, registration and the limits on standard properties. support.google.com/analytics/answer/10075209.
- Google Analytics Help. Sunset of Google Optimize. Official notice of the 30 September 2023 retirement, which is why this plumbing is manual today. support.google.com/optimize/answer/12979939.
- Kohavi, R., Tang, D. and Xu, Y. Trustworthy Online Controlled Experiments: A Practical Guide to A/B Testing. Cambridge University Press, 2020. Supporting material at experimentguide.com.
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.