Universal Analytics utilizes two components (by default) to attribute a browser session to a specific campaign: query parameters in the URL and the referrer string.

The page URL is sent with every hit to Google Analytics using the Document location field, which also translates to the &dl parameter in the Measurement Protocol.

The referrer string is sent with a hit to Google Analytics using the Document referrer field, as long as the referrer hostname does not exactly match that of the current page and the referrer string is not empty.

When attributing a hit to a campaign session, Google Analytics first checks the location value for the following parameters:

  • gclid - for attributing the session to Google Ads
  • utm_source - for attributing the session to a custom campaign

If these parameters are not found, Google Analytics looks at the referrer string.

Yes, there are additional steps to campaign attribution (see the flowchart), but these two are relevant in the context of this article.

There are two contexts where attributing campaigns based on URLs and referrer strings becomes complicated.

  1. If the site blocks Google Analytics on the landing page (due to missing consent), but then makes it available on subsequent pages (due to granted consent), the original campaign URL and referrer string are no longer available.
  2. If the site is a single-page application, particularly Google Tag Manager-based tags become susceptible to the rogue referral problem.

For this purpose, I have created a new custom template: Persist Campaign Data.

This template tackles both of the problems listed above. However, you will need to make additional changes to your Google Analytics settings before the solution is complete.

You can download the template from the template gallery.


The Simmer Newsletter

Subscribe to the Simmer newsletter to get the latest news and content from Simo Ahava into your email inbox!

The first problem is particularly bothersome on sites that utilize consent management systems to block Google Analytics if consent has not been granted.

If consent is granted on the landing page, then there should be no issues. The page is reloaded with the campaign parameters in the URL and the referral string intact, and Google Analytics tags can make use of these when attributing the session to a specific campaign.

However, if the user grants consent only after navigating deeper into the site, the tags that fire on these pages will no longer have access to the initial campaign that brought the user to the site, which leads to inflation of direct traffic.

To solve this problem, the template tag (assuming Google Tag Manager is not blocked, too) writes the URL of the current page in a cookie if it has campaign-setting parameters. Similarly, the referrer string is also written in a cookie if its hostname does not contain the current page hostname.

Then, when the Google Analytics tags finally fire, they can take the original location and referrer from these cookies to attribute the session to its correct source.

Setup the Persist Campaign Data tag

Create a new tag with the Persist Campaign Data template. Check the box next to Store campaign data in a browser cookie and check that the settings are OK.

By default, the following URL parameters will cause the tag to set the campaign URL cookie:

  • utm_source
  • utm_medium
  • utm_campaign
  • utm_term
  • utm_content
  • utm_id
  • gclid

Naturally, you can add additional parameters to the list if you wish.

Not all of these parameters have the ability to start a new session (only utm_source and gclid do, in fact), but the assumption is that if you’ve set any UTM parameters, you want to include them with your hits.

Next, you can change the default name of the URL cookie and the referrer cookie, if you wish.

NOTE! If you do change the name of either cookie, you must edit the template permissions and allow the template to set the new cookie name(s).

The tag will only write the cookies in specific conditions:

  1. The URL cookie is only written if the URL query string has any one of the listed trigger parameters. If such a parameter is found, the full URL is written into the campaign URL cookie.
  2. The Referrer cookie is only written if the referrer hostname does not contain the current page hostname, and the referrer string is not empty. If both conditions are good, the full referrer string is written into the referrer cookie.

Thus it’s safe to set it on the All Pages trigger, as it doesn’t matter if it fires on every single page.

Note! You might want to actually set it on a trigger that fires once user consent has been established - if the user gives consent or has already given consent, the tag does not need to (and should not) fire, because then your Google Analytics tags would fire normally and your wouldn’t need to save the URL and referrer data in cookies.

You’ll need two new, user-defined 1st Party Cookie variables.

The first, {{Cookie - __gtm_campaign_url}}. The second, {{Cookie - __gtm_referrer}}.

Make sure you check the URI-decode cookie box.

Create the customTask

To update your Google Analytics tags, we’ll be using a customTask. This is an easy way to make sure the cookies are only used if they exist, and it’s a great way to actually delete the cookies as soon as they’ve been used, so that they don’t persist needlessly.

NOTE! You don’t have to use a customTask. You can set the location, page, and referrer fields manually on the tag, if you wish. You just need to add proper fallback values if the cookies do not exist, otherwise you risk corrupting your data with invalid location and referrer fields.

If you need the cookies for something else as well, then you can set the customTask to not delete cookies after the values have been “used”.

Create a new Custom JavaScript variable, name it {{customTask - set location and referrer from cookie}}, and add the following code within:

function() {
  return function(customModel) {
    // Set to the cookie variables
    var storedLocation = {{Cookie - __gtm_campaign_url}};
    var storedReferrer = {{Cookie - __gtm_referrer}};
    // Set to the cookie names
    var storedLocationCookieName = '__gtm_campaign_url';
    var storedReferrerCookieName = '__gtm_referrer';
    // Set to the root domain of your site (e.g. domain name without any subdomain, "mysite.com")
    var domainName = 'simoahava.com';
    // Choose whether to delete a cookie after its value is used (true/false)
    var deleteCookieToggle = true;

    var deleteCookie = function(name) {
      document.cookie = name + '=; path=/; domain=' + domainName + '; expires=Thu, 01 Jan 1970 00:00:00 GMT';
    if (typeof storedLocation !== 'undefined') {
      customModel.set('location', storedLocation);
      customModel.set('page', document.location.pathname + document.location.search);
      if (deleteCookieToggle === true) { deleteCookie(storedLocationCookieName); }
    if (typeof storedReferrer !== 'undefined') {
      customModel.set('referrer', storedReferrer);
      if (deleteCookieToggle === true) { deleteCookie(storedReferrerCookieName); }

There are some things you might need to edit.

Change the storedLocation and storedReferrer variables to point to the correct Google Tag Manager 1st Party Cookie variables you’ve created, if you’ve diverted from the default values used in this guide.

Change the storedLocationCookieName and storedReferrerCookieName to the correct cookie names, if you’ve diverted from the default values used in this guide.

Set the domainName to the correct root domain (domain name without any subdomains included).

Set the deleteCookieToggle to false if you don’t want the task to remove cookies once their values have been “used”.

This customTask sets the location andreferrer fields if the respective cookies exist, and then (optionally) deletes the cookies for those fields that were set. If a cookie doesn’t exist, its field is not set.

If the location is set, the customTask also sets the page field to the current page path and query string. If it didn’t do this, then the tag would take the page path from the location as well, which would inflate the pageviews of the landing page rather than increment them for the page the user is actually on.

Add the customTask to your Google Analytics tags

Finally, add the customTask variable you just created to your Google Analytics tags. You only need to have it fire on the first tag that fires on the page, but just to be safe it might make sense to add it to your Google Analytics Settings variable, and then attach that to all your tags.

The field name is customTask and the value is {{customTask - set location and referrer from cookie}}.

Store campaign data - summary

If the user lands on your site with campaign parameters in the URL and/or a referrer that is external to your website’s domain, the following happens:

  1. The Persist Campaign Data tag creates new cookies where necessary; one for the URL of the page (if it had campaign parameters) and one for the referrer string (if it’s an external domain).
  2. Once the user gives consent, your Google Analytics tags utilize a customTask which sets the location and referrer fields from the cookies, and subsequently deletes the cookies.
  3. Thus the campaign URL and the referrer string are preserved for Google Analytics tags that might not fire until later in the site navigation, when the original URL parameters and referrer string are just a memory.

There are some caveats to note.

  1. If you already have a customTask set, you need to modify it to include the customTask code from this guide. See this for inspiration.
  2. There might be a race condition where the Persist Campaign Data tag completes after the Google Analytics tag has fired, leading to cookies being created but not utilized (and subsequently deleted). An easy way to avoid this problem is to make sure the Persist Campaign Data tag only fires if user has not given consent. That way it will only fire if the Google Analytics have been blocked.
  3. The cookies are session cookies so if the user closes the browser and then re-opens it to continue navigation, the stored data is lost.
  4. Cross-domain tracking is something that is very likely lost if you delay firing your tags beyond the landing page - this solution will not help with this problem. You could take the Client ID from the linker parameter, but since you’re not validating the linker parameter, the data would be vulnerable to link sharing (and any users who follow the shared link would be counted as the same user by Google Analytics).

2. Push original location in dataLayer

The rogue referral problem is a scenario where tags firing on a single-page app use the “virtual” location for campaign attribution (often missing the original campaign parameters) and thus Google Analytics reverts to the referrer instead. This results in sessions where hits sent on the first page are part of a google / cpc session, but as soon as the user navigates to other parts of the SPA, the campaign changes to the referral source, such as google / organic.

NOTE! Please understand that these steps must only be taken if the site is a single-page app or has single-page app components. There’s no harm in setting this up on a regular site, but it’s just extra work and clutter in the container.

To fix this, you need the following.

Setup the Persist Campaign Data tag

Create a new tag with the Persist Campaign Data template. Check the box next to Push original location in dataLayer and check that the settings are OK.

Set this tag to fire on the All Pages trigger.

This tag, when fires, does a very simple thing. It writes the current URL into the dataLayer using the key originalLocation.

It’s important that this tag only fires once on the initial page load (hence the All Pages trigger).

Create the Data Layer Variable

Next, create a Data Layer variable named {{Original Location}}, and set it to point to the originalLocation key. Importantly, set its Default Value to {{Page URL}}.

The Default Value is very important. It’s more than likely that a Google Analytics tag fires before originalLocation is pushed into dataLayer, so the variable simply returns the current page URL (which should be the same as originalLocation) in case such a race condition emerges.

Update your Google Analytics tags

Now that you have the template tag firing on the All Pages trigger, and you have the Data Layer variable ready, you need to edit every single one of your Google Analytics tags. Easiest way to do this is with the Google Analytics Settings variable.

NOTE! It’s vital that every single Google Analytics tag has this modification. If there’s even a single tag that fires without the fixed location, it’s possible that you campaign data will be broken.

You need to add two fields to the variable:

Field name: location
Value: {{Original Location}}

Field name: page
Value: {{Page Path}}

The value of page should be whatever you currently use for your virtual pageviews on your single-page app. It could be a Data Layer variable, a history state variable, or it could simply be the current page URL or current page path, as in the example above.

Both of these fields are required to be set in your tags.

How it works

Once you’ve set it up, here’s what happens.

  1. When the user lands on the page, the page URL is stored in the originalLocation Data Layer variable.
  2. When a Google Analytics tag fires, it takes the location field from this variable. This is because the location field is used for campaign attribution, and the landing page has all these parameters.
  3. The page field is set so that a virtual page path could be set for your single-page app page views.

Thus by always sending the URL of the initial page load as the value of the location field, the rogue referral problem is fixed.


The Persist Campaign Data template addresses two problems with campaign tracking in Google Analytics.

  1. Delayed / deferred Google Analytics tracking, where the first hits of the session are sent after the user has navigated away from the landing page.
  2. Single-page application tracking, where campaign sessions get incorrectly attributed to the referrer string rather than the parameters in the URL.

You can, of course, utilize the features for other things as well. Persisting campaign parameters can have uses beyond Google Analytics tracking, and identifying the URL of the first page load of a SPA could similarly be used to understand navigation patterns better.

As always, I look forward to hearing from you in the comments. Do you see problems with either approach? Do you have suggestions for improving the template or the implementation thereof?