by Doug


Internet Marketing

More Page Visibility with GA and GTM. Bending GA and GTM to my will.

My friends Simo and Yehoshua published articles recently regarding the use of page visibility in GTM and GA:

These articles are well worth a read. They are excellent articles in their own right and are good background reading to this post.

These articles really got me thinking. Simo’s statement at 19:45 in the video of his SuperWeek presentation is the key to this post:

“This isn’t hacking. This is just bending GA to your will.”

Simo’s point is that if the data captured and rendered in GA isn’t right for you or your business, think how you can change it to better represent reality – your reality.

The time spent on a page when it’s hidden or visible isn’t relevant to my business requirements. When a page is opened, regardless of whether it’s hidden or visible, I still want to track a pageview. I’m really interested in the visibility state of a page when a user lands it. If they open a page in a hidden state, I want to know if the page ever becomes visible. Finally, I want to know the visibility state of the page when it is closed.

  • Open – hidden or visible?
  • Is it ever visible?
  • Was it hidden or visible when closed?

Let’s take the techniques described in Simo’s and Yehoshua’s articles and ‘bend them to my will’.

I’ve always said:

Good artists borrow.

Great artists steal.

GA & GTM Setup

Start with the Visibility Prefix and Visibility Hidden variables to determine how the user’s browser flags visibility:

Now we’ll use a lookup table to neatly return the visibility state of the page:

So far so simple. Time to create a tag. We need to listen for changes to the page visibility state. This is pretty much the same visibility listener as previously documented by Simo and Yehoshua:

This tag fires on all pages. We’ll have an existing pageview tag that also fires on all pages. You could choose to record the visibility state as a custom dimension on all pageviews if you wanted. Just a little GA config and use of the {{Visibility Current Lookup}} variable will accomplish this:

This beasty records the visibility state in the event label ({{Visibility Current Lookup}}) and the nature of the pageview (Open, pagevew or exit) in the event Action.

Really important: this tag uses ‘useBeacon’. We need to use this to fire the event when a page exit occurs. We can’t use a normal /collect request and expect the tracking hit to fire reliably so we’ll rely on useBeacon instead. As you’ll see later on when I expose a link to the GTM container, the UA ID is extracted from the {{UA – Lookup}} variable. You’ll need to change this if you intend to use the container.

The trigger setup justifies some explanation. As mentioned earlier, this event will fire when the page is a landing page, exit page or when the visibility state changes from Hidden to Visible. The firing triggers are ‘Document Unload’, ‘Landing Page’ and ‘Page becomes visible’. The exception triggers are ‘Internal linkClick’ and ‘Visibility state recorded’.

Document unload and Internal linkClick

The purpose of Document unload and Internal linkClick is to be able to correctly identify page exits. We can’t rely on Document unload in isolation. Every time a user clicks from one page on your site to another page, one document has to ‘unload’ before the next is loaded. So, we need to find the unload events where a user has not clicked through to another page…

The Document unload trigger is actually a very simple Custom Event Trigger:

Where does the unload event come from though? We listen for unload events using a custom event listener (I’m sure I’ve seen something like this before you know…!). UTIL – Unload Listener is a custom HTML tag:

This tag also fires on all pages. When a ‘beforeunload’ browser event is detected, we push an unload event onto the dataLayer. This, in turn, enables the Document unload trigger to evaluate to true. Great, so now we’re able to spot the unload events but how do we know these aren’t just internal link clicks? This is where the exception trigger ‘Internal linkClick’ comes in:

Ah, so we have a variable ‘Internal Link Click’. If this evaluates to true then the trigger fires and the Page Visibility – event tag doesn’t fire. If the Internal linkClick trigger doesn’t fire then an internal link has not been clicked, the page is properly unloading and we fire Page Visibility – event to record the visibility state on page exit. Neat. We should also change the non-interaction nature of the event so that page exit visibility events are interactive which will give a more realistic time on page and session duration…that’s out of scope for now but should be a simple addition.

Anyhow, how is an internal link click detected? How does the Internal Link Click variable work? This is a piece of custom javascript:

We extract the current hostname from the {{Page Hostname}} variable. We also grab a piece of information from the dataLayer…this is magic! We know that if a link is clicked, we’ll have a gtm.linkClick event fire prior to the unload. So, we can look at the URL of the clicked element and compare the domain in the clicked URL with the current page domain. If the link click is heading off to a new domain, it’s an external link click. If there is no elementURL available in the previous dataLayer message then, again, there was no link click to an internal page and so we can determine that the page is being closed.

This variable will only return true if a link was clicked to a page on the same site. All other cases return false. Hence, we can determine if an internal link is clicked. BIG caveat here – watch out for subdomains or cross domain environments – you’ll need to tweak this variable to allow for other cases. This works in a simple single domain scenario quite nicely though.

Here’s how we see the previous message on the dataLayer – a variable called Previous Message:

Remember the dataLayer is an array (sort of). We can look at the penultimate message by referencing the array length minus 2th element of the array – that’s the message we’re interested in.

Previous Message elementURL pulls the gtm.elementUrl value from the Previous Message (or undefined if it doesn’t exist):

You can look at other values on the previous message in the dataLayer too. We don’t need to here, but we can look at the previous message event:

Landing Page

How do we know when we’re on a landing page? This is a question fraught with complexity and nuances that cause furious arguments on social media. This is my blog, my project, my site and my rules so I say a landing page is one where the referring hostname is not the same as the site I’m on right now:

Done. We track the visibility state in this use case.

Page becomes visible.

If a visibility change event is detected and we can determine what the new visibility state is, we can then say what the old visibility state was:

Hang on a minute though – a page could go from hidden to visible to hidden and back to visible again. I’m only interested in the first change from hidden to visible so we need an exception trigger to block the tag when we’ve already tracked the visibility state change:

Two things: First, the trigger only fires when the ‘Visibility State Change Recorded’ variable returns a value of ‘true’. Second, isn’t it annoying when variable or trigger names are truncated in select elements?

Try adding this script as a bookmarklet in your browser:

javascript:jQuery(‘.gtm-field-group select.inline’).css(‘min-width’,’300px’);

Then, when you’re on a trigger form where the truncated names are shown you can execute the bookmarklet and you’ll see wider fields:

Better? Good.  All the ‘Visibilty State Change Recorded variable does is to rifle through the dataLayer to see if any visibilityChange events are present:

Hence, if the visibility state has changed in the past on this page, we won’t record any more changes to ‘Visible’ past the first one.

Pulling the assets together

Now we have a set of tags, triggers and variables. Let’s pull them together in a full solution. The only variable we haven’t discussed so far is ‘Visibility State Event’. The event action in the ‘Page Visibility – event’ tag is set by {{Visibility State Event}}. The functionality of this variable neatly describes the use cases where and how the tag fires:

Assume the user lands on our page by typing in The event is not ‘unload’, there is no referring domain, the visibility state is visible and hasn’t changed. The end result is a non-interaction event registering the event action as ‘Open’ and the label as ‘Visible’.

If the user opens the page from Google search results in a new background tab, the same event fires and the label is ‘Hidden’. If the user then activates the tab then the GTM event is visibilityChange, the GA event action is ‘Pageview’ and the label is ‘Visible’.

The user may click through to another page on the site. An unload event fires but the previous dataLayer message was a gtm.linkClick to another page on so the event does not fire yet. Having arrived at the next page, the referring domain is seen to contain the string conversionworks so a ‘Visible’ ‘Pageview’ is recorded.

Finally, the user decides to close the browser tab. No link was clicked so the unload trigger fires the GA event with an action of ‘Exit’ and a label of ‘Visible’.

That’s how it works. Now take a look at the data.

The data

Okay, how does it look? Landing pages first. Remember the custom dimension? See how the Landing Page report looks with an extra dimension:

Great – now we can see which pages are being opened in visible and hidden states. Interesting how the engagement metrics vary. I suggest the homepage is being opened in a hidden state for a while before users get round to looking at it. It’s clear that longer sessions result from the homepage being opened in a visible state. Is the visibility state on a landing page a signal regarding engagement and intent perhaps? More data and testing will tell but this is just the kind of question that this data poses.

This table is easily filterable to show only hidden or visible landing pages. I’m interested in which pages are opened hidden:

Look at /croDay ( if you’re interested!) – opened hidden but not a bounce – users interacted with this page – worth digging into. Some older blog posts there – time for a prune?

What about the events?

Hmm! Much more Open and Exit with visible than not – a good sign. Equally – plenty of Pageview Visibile too. And the pages? If we click through on the Pageview event action and then change the primary dimension to page we’ll see the list of pages that were opened hidden and then were viewed by users:

I expect the homepage to encounter a number of users who open it in the background and then click through to it. Contact is a little curious but unsurprisingly the blog posts exhibit this behaviour. There is a lot of material on the internet regarding these subjects – they are commoditised.

What about the custom dimension on the Pages reports? How are users engaging with content when it is visible? Which are the most visible pages on the site?

Again, we’ll use the custom dimension as the secondary dimension and apply the custom table filter. Why a table filter and not a segment? The custom dimension is hit level –  we’re interested in page/hit level events. Hidden/visible values can vary across sessions. We only want to see data for when the pages were visible at one point in time:

Right, there’s /croDay with a visible status. Good. The other pages are expected to be visible at some point – no great surprises here but we’ve implemented this measurement on client’s GTM implementations and have been quite surprised. Retailers of household goods may expect to have a lot of comparison shopping – users opening multiple pages from Google search results and hopping between pages to compare sites. This is not always the case. Expectations and data don’t always marry up and it’s fantastic to see the truth.

Without digging into the data and the stories it’s telling us right now you can see how clean the data is and what is might tell you. This is crucial. The data is clean and therefore really easy to parse and rationalise. You can also see how the hidden/visible state of landing and exit pages can tell you a lot about how users are interacting with your site. KNOW this – don’t just think and hope. Act on the facts. Now you can.

The container

There is a lot of meat in this post. Lots of complexity normally equals big potential for ‘finger trouble’. I want to help prevent this for you so here’s a link to the container that contains the assets discussed so far GTM-VH.json

As mentioned previously, you’ll probably want to change the UA ID variable or at least the values in it. You’ll also need to change the value of the domain in the Landing Page trigger so it matches your domain(s).

Your import should look something like this:

As usual, don’t import this straight into a production container. Test it on a test or dev environment. It’s V2 only. No, I won’t provide support unless there is something ridiculously wrong with it. Don’t do anything stupid with this container. Respect your GTM assets and your clients’ GTM assets and data.

Sharing ideas is cool but don’t expect me to respond to feature requests immediately. Having said that, anything is possible…it just gets more expensive 🙂


I hope you find these techniques as useful as I have. Yes, I’m keen to see the hidden/visible metrics on this blog post!

Credit goes to Yehoshua and Simo again for their posts – very valuable in their own right.

You CAN bend GTM and GA to your will with some thought and experimentation.


Leave a comment

Your email address will not be published.

ConversionWorks is now Media.MonksVisit us at