Skip to main content

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.

Note: GrowthBook also offers a visual editor for lightweight, no-code UI changes.

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, cookies, etc; as well as the ability to track events (GA or otherwise).

Choosing a strategy: scalable and easy versus page speed

There are two strategies that we can use to implement AB tests with GrowthBook via GTM.

The first strategy is scalable and easy, but comes with a page render speed tax. Using this method, we implement a single code snippet that only needs to be updated when we need to instrument new on-page variations, which are associated with feature flags. Our code snippet will query GrowthBook for all current AB test experiments and their rulesets, and will automatically evaluate feature flags against them. 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 our on-page features.

The second strategy does not incur as much of a page render speed tax - the flicker is much less noticeable. However, you’ll need to hard-code all of your experiments and their rulesets, as well as any feature flag rules and overrides, into the GTM tag’s source code. Each time you need to change your experiments or phases, you’ll need to modify the code snippet and publish a new GTM version. For some, this may impose a significant burden; for others, the render speed improvement may be worth the tax.

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.

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.

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
}
})();
</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 apiKey, 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.

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: Scalable and easy

Recall that our first option was to provide a more standard implementation that we can scale easily to a higher volume of AB tests. Also recall that this strategy comes with some page flickering while we await feature flag definitions from the GrowthBook app before rendering our variations.

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 comment with a snippet that will fetch the feature flag definitions and then 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({
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 === "variation-1") {
document.querySelector("banner1").innerHTML = "Click here in the next 24 hours";
} else if (value === "variation-2") {
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.

Strategy 2: Page speed optimized

We can eliminate the gb.loadFeatures() call from the first strategy, thus reducing the render flickering, if we are comfortable implementing our own inline experiments and their render treatments. This approach, while performant, is less scalable because it requires more manual modification of GTM tags for each new experiment run or for changes in targeting, rollout, etc.

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({
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 === "variation-1") {
document.querySelector("banner1").innerHTML = "Click here in the next 24 hours";
} else if (res.value === "variation-2") {
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 verbose and inflexible, this approach is performant and allows us to avoid the gb.loadFeatures() call. 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.