Using GrowthBook with Google Tag Manager (GTM)
Now customers who are familiar with feature management using Google Tag Manager (GTM), yet may lack the engineering resources or capability to implement changes in their codebase, may easily use GrowthBook with GTM to power their AB tests. This setup is commonly used by marketing teams and CRO agencies.
GrowthBook also offers a visual editor for lightweight, no-code UI changes. This approach is compatible with GTM and the examples provided on this page as long you enable the "Include Visual Experiments" toggle within your SDK Connection.
In this guide, we will assume familiarity with the GTM platform. We also assume the ability to apply a unique user ID via Google Analytics (client_id
), cookies, etc; as well as the ability to track events (GA or otherwise).
Choosing a strategy: performance vs flexibility
There are two strategies that we can use to implement AB tests with GrowthBook via GTM.
The first strategy is the most performant but requires us to hard-code all of our experiments and their rulesets instead of referencing feature flags. While this can lead to some inflexibility in controlling targeting post-launch, the upside is that we avoid an extra network call to GrowthBook to fetch feature flags. This leads to less noticeable flicker between our initial page render and the DOM changes we apply after the GrowthBook SDK loads. In this strategy, each time you need to change your experiments or targeting rules, you'll need to modify the code snippet and publish a new GTM version.
The second strategy gives us the flexibility of using feature flags, but comes with a page render speed tax. Using this method, we implement a single code snippet that needs to be updated whenever we need to instrument new on-page variations for experiements, which are associated with feature flags. Our code snippet will query GrowthBook for all feature flags, which would typically reference AB test experiments and their rulesets, and will automatically evaluate these feature flags for us. However, we need to make a network request to the GrowthBook app to fetch these features, and this happens client-side while the page is loading. By the time we receive a server response and can apply DOM changes, there may be a noticeable flicker while re-rendering.
If neither of these strategies seem ideal, we'd encourage you to pursue a traditional GrowthBook implementation without GTM, preferably with back-end or hybrid feature flag evaluation. See our SDK documentation for more information.
Basic setup
Regardless of which of the above strategies you've chosen, in order to implement GrowthBook AB Tests you'll first need to inject the GrowthBook Javascript SDK using a GTM Tag. You'll also need to pass some basic information along to the SDK in order to use it.
Creating a GTM tag for the GrowthBook SDK
First, create a new tag in your desired workspace. We will choose "Custom HTML" as the tag type. We can give it the name "GrowthBook SDK" or similar. Also, be sure to set the firing triggers to target the specific pages where we need to instrument our feature changes and experiments (or just choose "All Pages").
Next, paste in the script below to load the GrowthBook JavaScript SDK; then save the tag:
<script id="growthbook-sdk" src="https://cdn.jsdelivr.net/npm/@growthbook/growthbook/dist/bundles/index.min.js" defer></script>
To publish the SDK tag, submit our workspace changes (the blue "Submit" button on the top of the GTM application), Then ensure "Publish and Create version" is selected and click the blue "Publish" button – or use whichever GTM release strategy you are already using.
Initializing the SDK
To actually use the GrowthBook SDK on our pages to control on-page features with AB tests, we'll need to create another code snippet and load it through GTM. While you can certainly add the snippet to your existing tag (the GrowthBook SDK tag we created above), you may find it cleaner to add another Custom HTML tag and give it a name such as "GrowthBook Implementation" Be sure to set the firing triggers in the same way as you did above.
In the subsequent sections, we will learn how to fill in the SDK parameters such as our own apiHost
, clientKey
, attributes
, and trackingCallback
. For now, insert the following code into your tag:
<script>
(function() {
// Wait for the SDK to load before starting GrowthBook
if (window.growthbook) {
startGrowthbook();
} else {
document.querySelector("#growthbook-sdk").addEventListener("load", startGrowthbook);
}
function startGrowthbook() {
if (!window.growthbook) return;
var gb = new growthbook.GrowthBook({
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abcd1234",
// TODO: Add decryptionKey if using encryption
attributes: {
id: "u1234" // TODO: Read user/device id from a cookie/datalayer
},
trackingCallback: function(experiment, result) {
// TODO: track experiment impression
}
});
// TODO: Instrument DOM with AB test logic
gb.loadFeatures().then(function() {
// NOTE: We may wish to remove `gb.loadFeatures()` and instead manually implement experiment logic
});
}
})();
</script>
Let's have a look at the SDK constructor code above, specifically the parameters defined within this block:
var gb = new growthbook.GrowthBook({
apiHost: "https://cdn.growthbook.io",
// etc...
});
Here, we will need to add pass in SDK connection parameters, user attributes, and any tracking callback functions. We will talk through each of these.
Connection parameters
Let's first pass in our SDK connection parameters. To find them, you will need to find your SDK in the GrowthBook app (in Features > SDKs), and select "Javascript" for implementation instructions. Here you will see values for apiHost
, clientKey
, and in some cases decryptionKey
. (Note: if you've decided to use Strategy 2 - page speed, you won't actually need to include these connection parameters.)
User attributes
We also need to define any relevant user attributes, most importantly the id
. If you have any other user attributes available that affect your experiment targeting, also add them to the attributes
object here.
Let's talk about the user ID specifically, since it's crucial for running AB tests wherein individuals are deterministically bucketed into experiment variations. Skip ahead if you already have a good way to retrieve a unique user ID.
User id from your website
Chances are your website has an internal userId associated with the user's session. If you are able to obtain this ID in JavaScript, use this value for your GrowthBook user attributes. For example, the userId may be assigned to all rendered pages via something like this (PHP template, although your site may use an entirely different server configuration):
<script>
var myWebsiteUserId = "<?= $_SESSION['userId']; ?>";
</script>
You could then assign this value to your GrowthBook user attributes:
attributes: {
id: myWebsiteUserId
}
Alternatively, you could potentially pull the userId out of your site's session cookie, if available. A sample PHP-based session system might have a JavaScript lookup like this:
<script>
var sessionCookie = document.cookie.match(/PHPSESSID=([^;]+)/);
var myWebsiteUserId = sessionCookie
? decodeURIComponent(sessionCookie[1]).match(/userId=([^&]+)/)[1]
: null;
</script>
User id from Google Analytics
For organizations using GTM, Google Analytics is also commonly used (which GrowthBook plays nicely with via a BigQuery integration), although there is certainly no requirement that you also use GA with GrowthBook. With this configuration, it's often common to see a client_id or user_id associated with GA.
GA4
You may be interested in getting the client_id (a unique identifier for users and their device) to represent a single user. This is available out of the box with GA4. The "proper" way to retrieve it in JavaScript would look like this (where G-XXXXXX
is your GA4 property ID):
var clientId = "";
gtag('get', 'G-XXXXXX', 'client_id', function(cid) {
clientId = cid;
});
However since this is an asynchronous lookup, we need to make sure that clientId is available before we create the GrowthBook SDK instance. The simplest way to do this is to nest the SDK instantiation inside your gtag callback function. For example:
gtag('get', 'G-XXXXXX', 'client_id', function(cid) {
var gb = new growthbook.GrowthBook({
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abcd1234",
attributes: {
id: cid
},
// etc...
});
// TODO: Instrument DOM with AB test logic
});
...or you could use async/await or promises to prevent additional nesting.
In some cases where gtag()
is not yet defined at the time in which we invoke it, we may need to force our GrowthBook snippet to load after GA has successfully loaded. You may be able to use window.onload()
to ensure GA has loaded before the SDK is instantiated (although GA loaded through GTM can sometimes complicate this). Alternatively, you can use a GTM trigger such as "Window Loaded" to ensure that the GrowthBook SDK is loaded after GA has loaded.
All of these delays, however, can cause render flickering and should be avoided if possible. You might consider loading GA outside of GTM. You could even load GA and GrowthBook in parallel, wrap each load inside its own promise, and use Promise.all()
to ensure both are loaded before the SDK is instantiated. Specifics will vary depending on your site's configuration, but hopefully this provides some ideas.
In some situations, gtag()
may never become defined. You can use your browser's developer tools / JavaScript console to confirm this by typing window.tag
. If undefined, you may need to manually define it — either on your page or in a GTM custom HTML tag — using JavaScript. For example:
window.gtag = window.gtag || function() { dataLayer.push(arguments); };
You may have a custom user ID (website-generated perhaps) that you'd like to propagate through GA4. In this case, you'll want to ensure that this user ID has been pushed to the dataLayer (GA4's local data store). For example:
dataLayer.push({
'user_id': myWebsiteUserId
});
Then, to get the user id from GA4 into our GrowthBook user attributes, we can modify our SDK code snippet:
gtag('get', 'G-XXXXXX', 'user_id', function(uid) {
var gb = new growthbook.GrowthBook({
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abcd1234",
attributes: {
id: uid
},
// etc...
});
// TODO: Instrument DOM with AB test logic
});
GA (Universal Analytics)
Prior to GA4, the mechanism to fetch our client_id is slightly different. A few different ways to deal with this are via a lookup using the GA API:
attributes: {
id: ga.getAll()[0].get('clientId')
}
...or by extracting the ID from the cookie:
attributes: {
id: document.cookie.match(/_ga=(.+?);/)[1].split('.').slice(-2).join('.')
}
Tracking callback
We may be interested in defining an analytics tracking event in order to track AB test impressions. This is not required for GrowthBook to function correctly, but is often of interest for 3rd party analytics purposes.
If you already have tracking events set up, insert them into our code snippet. Here's a contrived example:
trackingCallback: function(experiment, result) {
TrackingAgent.track("experiment_viewed", {
experiment_id: experiment.key,
variation_id: result.variationId,
userId: myWebsiteUserId, // or perhaps client_id from gtag('get', 'client_id', ...)
});
}
Or if you are using Google Analytics (GA4) for event tracking, it may look something like this:
trackingCallback: function(experiment, result) {
gtag("event", "experiment_viewed", {
event_category: "experiment",
experiment_id: experiment.key,
variation_id: result.variationId,
});
}
Note the omission of client_id
, as this is typically included by default in GA events.
We're now ready to start implementing our AB test rendering logic. Let's return to the two aforementioned implementation strategies...
Strategy 1: Page speed optimized, inline experiments
Recall that in this strategy, we define our experiments and their rulesets inline. This avoids an extra network request via gb.loadFeatures()
, which introduces an additional delay. Avoiding this lookup ensures any render flickering is minimized.
Let's get started with our test instrumentation. We'll consider a hypothetical landing page where there are 2 features that need to be changed on the DOM based on feature flag settings: a large button (#button1) which we can optionally turn green, and a sticky banner (#banner1) which we can alter with custom text.
Find the section of our snippet above beginning with // TODO: Instrument DOM with AB test logic
. We will replace this section, including the loadFeatures() call, with a snippet that will run an inline experiment and apply test-specific DOM changes. Our earlier code snippet now becomes:
<script>
(function() {
// Wait for the SDK to load before starting GrowthBook
if (window.growthbook) {
startGrowthbook();
} else {
document.querySelector("#growthbook-sdk").addEventListener("load", startGrowthbook);
}
function startGrowthbook() {
if (!window.growthbook) return;
var gb = new growthbook.GrowthBook({
attributes: {
id: "u1234" // TODO: Read user/device id from a cookie/datalayer
},
trackingCallback: function(experiment, result) {
// TODO: track experiment impression
}
});
// 2-way inline test
var result1 = gb.run({
key: "button-experiment",
variations: ["control", "green-button"],
weights: [0.5, 0.5], // Traffic split between the variations
coverage: 1.0 // What percent of overall traffic to include (0.0 to 1.0)
});
if (result1.value === "green-button") {
document.querySelector("#button-1").classList.add("green");
}
// 3-way inline test
var result2 = gb.run({
key: "banner-experiment",
variations: ["A", "B", "C"],
weights: [0.5, 0.25, 0.25],
coverage: 1.0
});
if (result2.value === "B") {
document.querySelector("banner1").innerHTML = "Click here in the next 24 hours";
} else if (result2.value === "C") {
document.querySelector("banner1").innerHTML = "Click here before this offer expires";
}
}
})();
</script>
Note that we have needed to define the properties of both our button-experiment
and banner-experiment
inline. Although less flexible than using feature flags defined in GrowthBook, this approach is performant and relatively easy to implement. The gb.run()
call, which evaluates a feature flag to determine which AB test variant a user belongs to, is evaluated on the client side and does not make a network call.
Strategy 2: Flexible feature flags, less performant
Recall that our second strategy allows us the flexibility of referencing feature flags and their targeting rules, which we may modify in the GrowthBook app without pushing an update to our GTM tag, even after the test has launched. Also recall that this strategy comes with additional page flickering while we await feature flag definitions from the GrowthBook app before rendering our variations.
In this scenario, our updated code snippet would look something like this:
<script>
(function() {
// Wait for the SDK to load before starting GrowthBook
if (window.growthbook) {
startGrowthbook();
} else {
document.querySelector("#growthbook-sdk").addEventListener("load", startGrowthbook);
}
function startGrowthbook() {
if (!window.growthbook) return;
var gb = new growthbook.GrowthBook({
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abcd1234",
// TODO: Add decryptionKey if using encryption
attributes: {
id: "u1234" // TODO: Read user/device id from a cookie/datalayer
},
trackingCallback: function(experiment, result) {
// TODO: track experiment impression
}
});
gb.loadFeatures().then(function() {
// 2-way test using a boolean feature flag
if (gb.isOn("green-button")) {
document.querySelector("#button-1").classList.add("green");
}
// Multiple variations using a string feature flag
var value = gb.getFeatureValue("my-string-feature");
if (value === "B") {
document.querySelector("banner1").innerHTML = "Click here in the next 24 hours";
} else if (value === "C") {
document.querySelector("banner1").innerHTML = "Click here before this offer expires";
}
});
}
})();
</script>
Here, we are instrumenting two different tests. First is our button (button-1
) which is modified by a 2-way AB test (control and one variant). It uses a boolean flag (green-button
) to toggle a CSS class. Second is our sticky banner (banner-1
) which is modified by a 3-way test. It uses a string flag (my-string-feature
), with values representing different test variations, to alter the text of our banner.
Notice that we need to call gb.loadFeatures()
and wait for the return from the GrowthBook app before actually implementing our AB test's render logic. This delay is responsible for some render flickering while switching from a control to a variant for our tests.