Track Internal Campaigns to Google Analytics using Google Tag Manager


If you use Google Analytics (GA) in your website, then you’re probably already tracking your external campaigns properly. But are you tracking your internal campaigns as well? These are marketing campaigns that you run within your website, like homepage banners that link to promoted products.

External campaigns are tracked with GA’s built-in utm_source, utm_medium, utm_campaign, utm_content and utm_term parameters to identify the marketing campaigns that drive users to your website.

However, for internal campaigns, you shouldn’t use these utm_source / utm_medium / etc tracking. If you do, GA replaces your external campaign tracking with these internal ones.

There are four ways to track internal campaigns in GA:

Enhanced Ecommerce’s PromotionsDesigned for internal campaign reporting, built into GAReport is limited to Ecommerce’s metrics
EventsQuick to setup and understandLimited to clicks on links, doesn’t encapsulate entire user behaviour
Site SearchNo coding neededFunction isn’t meant for internal campaigns
Custom DimensionsDesigned for business-specific reporting needs, flexible usageSome coding required

I personally prefer the Custom Dimensions method because it makes the most sense. It doesn’t force Events nor Site Search to be adapted for campaign tracking. And it can be used in reports that contain other dimensions and metrics.

And it can be done fairly easily with Google Tag Manager (GTM). Let me show you how.

Google Tag Manager at your service!

There are numerous examples on the Web that teach you to track internal campaigns with GA’s Custom Dimensions. But these normally require you to add code to your pages. If you’re not a developer or don’t have access to one, then this can hinder your work.

Logic for Tracking Internal Campaigns

The logic for tracking internal campaigns is similar to how GA does it for external campaigns:

  1. Get the internal campaign code from the URL.
  2. Save the campaign code to a cookie for a number of days.
  3. In the GA tag, set a Custom Dimension with the campaign code.

My solution assumes the following:

  • The internal campaign code is set in the “intcmp” URL query.
  • The cookie is saved for 30 days.
  • The internal campaign code is tracked to GA’s Custom Dimension 11.

GTM Setup

My solution is made up of seven variables and one tag. Here’s how they’ve all been setup.


I define 4 constants to make the rest of my solution easier to manage.

CS_internalCampaignURLQueryName of the internal campaign code URL query.
Change this based on your internal campaign query.
CS_GAInternalCampaignCodeDimIdxIndex of the GA Custom Dimension to track internal campaigns.
Change this based on your GA configuration.
CS_internalCampaignCookieNameName of the internal campaign cookie._gtm_intcmp
CS_internalCampaignCookieDaysNumber of days to keep the internal campaign cookie.
Change this based on your internal campaign duration.

Next, I define the variables for my URL query and cookie:

URL_internalCampaignQueryGets the internal campaign code from the URL query.
  • Variable Type: URL
  • Component Type: Query
  • Query Key: {{CS_internalCampaignURLQuery}}
CK_internalCampaignCodeCookie to store the internal campaign code.
  • Variable Type: 1st Party Cookie
  • Cookie Name: {{CS_internalCampaignCookieName}}

Notice that I use Constant variables to specify the URL query key and 1st Party Cookie name. This makes it easy to change the key and name respectively. For example, if I want to use a different cookie name, I only need to change the CS_internalCampaignCookieName variable, and everything else will automagically use that new cookie name.

Finally, I need a short Custom JavaScript to get the internal campaign code.

CJS_internalCampaignCodeGet the internal campaign code from the URL query or the cookie.
Custom JavaScript:
function() {
  return typeof {{URL_internalCampaignQuery}} === 'undefined' ?
    {{CK_internalCampaignCode}} :

This script gets the internal campaign code in one of two ways:

  1. If the URL contains the query key, get the code from the URL query.
  2. Otherwise, get it from the cookie.

“But what if the cookie is also not set?”
In this case, GTM returns a null value finally. Then in your GA tag, GTM will simply not set your Custom Dimension at all. This means that your Custom Dimension’s report is limited to internal campaign codes from the URL or the cookie only.

“Why is this script needed in the first place?”
This is to prevent a “race condition”. Let’s say the user opens an internal campaign URL for the first time. So before clicking, he doesn’t have the cookie. Your web page loads in his browser and GTM runs. GTM then does two things: it sets the cookie AND it fires your GA tag. But due to the asynchronous nature of GTM, the cookie may not have been set at the time that the GA tag is fired. However, I can safely assume that the internal campaign code is present in the URL, which will be saved to the cookie. So this script allows me to get the internal campaign code from the URL, even though GTM has not been able to set the cookie yet.


Custom HTML tag

There is one Custom HTML tag to set the internal campaign cookie. This is how it works:

  1. When the page loads and the URL query contains an internal campaign code, grab it. Otherwise, stop.
  2. If the internal campaign cookie is absent OR it does not have the same internal campaign code as the URL query, then save the cookie with the URL query’s code for the specified number of days.
  • Tag Name: HTML – set internal campaign cookie – all pages
  • Trigger: All Pages
"use strict";
var intCmpQuery = {{URL_internalCampaignQuery}};
var replaceIntCmpCookie = typeof intCmpQuery !== 'undefined' &&
  (typeof {{CK_internalCampaignCode}} === 'undefined' ||
  {{CK_internalCampaignCode}} !== intCmpQuery);

if (replaceIntCmpCookie) {
  // set the internal campaign cookie to intCmpQuery
  var date = new Date();
  date.setTime(date.getTime() +
    ({{CS_internalCampaignCookieDays}} * 24 * 60 * 60 * 1000));
  var expires = date.toGMTString();
  document.cookie = {{CS_internalCampaignCookieName}} + '=' + intCmpQuery +
    '; expires=' + expires;

Notice again that I use the Constant variables created earlier. This lets me customise things like the URL query key or cookie name without needing to modify this Custom HTML tag.

Google Analytics tags

If you haven’t done so already, create a new Custom Dimension in GA. Set its scope to “Hit”. Take note of its index and update the CS_GAInternalCampaignCodeDimIdx variable accordingly.

Then, in each of your GA tags, go to More Settings > Custom Dimensions, and add a new Custom Dimension:

  • Index: {{CS_GAInternalCampaignCodeDimIdx}}
  • Value: {{CJS_internalCampaignCode}}

This enables Google Analytics to track the internal campaign code to the Custom Dimension.

“Why use a Hit-level scope instead of Session or User?”
Recall that your internal campaign code needs to persist for a number of days. This duration is not the same as that of a Session or User. So it is best to set the code with every Hit. In this way, GA will alway track the code over the entire duration.

Test and Publish

As always, remember to test your changes before publishing!

Advanced Implementation

My solution is limited to tracking internal campaigns with one URL query only. It can be extended to include more than one query, e.g. int_source, int_content, etc. You can then save these to one or many cookies.

Some websites use many different URL query keys for internal campaigns. For example, some links may use “int_campaign”, while others use “int_cmp”. If your website does this, you can adapt my solution to handle these different keys and still track to the same Custom Dimension.

Have you implemented this?

I’d love to hear your feedback! Leave a comment with your experience or questions and we can collaborate together.

  • Sign up
Lost your password? Please enter your username or email address. You will receive a link to create a new password via email.
We do not share your personal details with anyone.