Here’s an interesting and hacky use case for you. It’s all about uncovering bounce metrics for visits which originate from organic Google search results. In particular, the metric we’re interested in is how long user dwelled on the landing page after arriving from organic Google search AND returned to the search engine results page (SERP) using the browser’s back button.

The inspiration for this post came from an audience question at the Best Internet Conference in Lithuania, which I recently attended as a speaker. They were concerned that Google is using Bounce Rate as a search ranking signal, and I was fairly strongly opinionated that it’s simply not possible, as all the “native” GA metrics are really easy to manipulate. However, Dr. Pete from Moz wrote about dwell time in 2012, and it makes a lot of sense. Google should be very interested how long the visitor stays out of the SERP when following a link. If users tend to immediately return to the SERP, it’s very likely the result was not relevant for them.

So, inspired by this question, I wanted to see if I can get some metrics from how long people dwell on these landing pages before returning to the SERP. I got my results, but it’s definitely not an easy problem to solve. As usual, we’re using a combination of Google Tag Manager and Google Analytics to perform the operation.

(UPDATE 17 April 2016 I updated this article, as I had to make some modifications to the code. It’s slightly more robust now, and it lends itself better to e.g. Custom Metrics, if you prefer to use those instead of User Timings.)

The result is a list of User Timings, where each landing page can be scrutinized against the time users spent on there before clicking the browser’s back button.

The mystery of the history

The difficulty is that when the user clicks the browser’s back button, we have no knowledge of where the user is taken to. That’s browser security for you. It would be highly questionable if the website had access to the history of the web browser. Makes sense, right?

Another difficulty is the actual browser back button. It’s not part of the browser object model, so we can’t actually measure clicks on it. Instead, we can infer a back button click from how people navigate with URL hashes! When a hash change is recorded, we can infer that it was due to browser back if we implement a little hack, where the hash is unique to organic Google search.

You see, by creating a new browser history entry for people landing from organic Google search, the back button click doesn’t take them to the SERP, but rather to the original landing page that existed before we redirected the user to the new history state. Using that as an indicator, we can extrapolate that the hash change occurred due to a browser back button click (or the backspace).

The process

The process is as follows:

  1. If the user lands on the site through organic Google search, create a new browser history entry with the hash #gref

  2. Later, if a hash change is registered, and the user is still on the landing page, and the hash change is from #gref to a blank string, fire a Google Analytics timing event, after which programmatically invoke the “Back” event in the browser history

Looks simple (actually, doesn’t look simple at all), but it’s very hacky indeed. You see, we’re manipulating browser history by creating a new, fictional entry called #gref. Then, when the user clicks browser back, instead of taking them back to Google search, it actually takes them to the previous state, which is the URL without the #gref.

THAT’S how we know both that the user clicked browser back AND that they were trying to return to the SERP. All we have to do is send the Google Analytics timing hit, and then move the user manually to the previous entry in the history (i.e. the SERP).

Why is it hacky? Well, you’re manipulating browser history, for one. You’re creating a custom state, and you’re forcing the user to adopt that state if they land from organic search. Next, you’re intercepting a legitimate browser back event, and instead of letting the user directly leave the site, you’re forcing them to send the GA timing hit first, before manually whisking them back to the SERP.

Whew! Lots of things that can go wrong. Luckily the JavaScript is solid and beautiful, but don’t forget to test thoroughly!

Wait, let me repeat that: test thoroughly. Also, if you’re running a single-page website, I can almost promise you that this won’t work out-of-the-box.

The Custom HTML Tag

At the heart of this solution is a single Custom HTML Tag. It’s fired by two different events, to which we’ll return shortly.

First of all, here’s the code:

<script>
  (function() {
    var s = document.location.search;
    var h = document.location.hash;
    var e = {{Event}};
    var n = {{New History Fragment}};
    var o = {{Old History Fragment}};
    
    // Only run if the History API is supported
    if (window.history) {

      // Create a new history state if the user lands from Google's SERP
      if (e === 'gtm.js' && 
          document.referrer.indexOf('www.google.') > -1 && 
          s.indexOf('gclid') === -1 &&
          s.indexOf('utm_') === -1 &&
          h !== '#gref') {
        window.oldFragment = false;
        window.history.pushState(null,null,'#gref');
      } else if (e === 'gtm.js') {
        window.oldFragment = true;
      }

      // When the user tries to return to the SERP using browser back, fire the
      // Google Analytics timing event, and after it's dispatched, manually
      // navigate to the previous history entry, i.e. the SERP
      if (e === 'gtm.historyChange' && 
          n === '' && 
          o === 'gref') {
        var time = new Date().getTime() - {{DLV - gtm.start}};
        if (!window.oldFragment) {
          dataLayer.push({
            'event' : 'returnToSerp',
            'timeToSerp' : time,
            'eventCallback' : function() {
              window.history.go(-1);
            }
          });
        } else {
          window.history.go(-1);
        }
      }
    }
  })();
</script>

Let’s quickly walk through this code. First of all, the whole block is encased in an immediately invoked function expression (IIFE) (function() {...})();, which protects the global namespace. Also, the whole solution only works if the user’s browser supports the History API: if (window.history) {...}.

The first important code block is this:

if (e === 'gtm.js' && 
    document.referrer.indexOf('www.google.') > -1 && 
    s.indexOf('gclid') === -1 &&
    s.indexOf('utm_') === -1 &&
    h !== '#gref') {
  window.oldFragment = false;
  window.history.pushState(null,null,'#gref');
}

This code checks the following:

  1. Was the Tag fired due to the Page View Trigger (i.e. a page load)?

  2. Did the user land from a Google site (referrer contains www.google.)?

  3. If they did, make sure it’s not from an AdWords ad (check that the URL does not have ?gclid) or custom campaign.

  4. Make sure also that the URL does not already contain #gref, which would imply the user followed a link with that hash or that the user already had a history entry with #gref, meaning they’ve navigated somewhere else within or outside the site after landing on it in the first place from the SERP.

If these checks pass, then a new global variable oldFragment is initialized with the value false. This means simply that this is a brand new landing on the site through organic Google search, and we check against this when we push the payload to dataLayer. We only want to send the Timing hit for landing page bounces.

Finally, a new browser history state is created, where the URL is appended with #gref to show that the user landed from organic Google search.

The next code block is:

else if (e === 'gtm.js') {
  window.oldFragment = true;
}

Here, we check if the event is a page load again, but the URL already has #gref. In this case, we set the global variable to true, since obviously this entry is not a direct landing from the SERP but something else. This way, we’ll block the Timing hit from happening, as we only want to measure true landing page bounces.

The final code block is:

if (e === 'gtm.historyChange' && 
    n === '' && 
    o === 'gref') {
  var time = new Date().getTime() - {{DLV - gtm.start}};
  if (!window.oldFragment) {
    dataLayer.push({
      'event' : 'returnToSerp',
      'timeToSerp' : time,
      'eventCallback' : function() {
        window.history.go(-1);
      }
    });
  } else {
    window.history.go(-1);
  }
}

Again, there’s a checklist of things:

  1. Is the event a browser history event?

  2. Is the old hash #gref and the new hash a blank string?

If all these checks pass, it means that the user tried to go back in browser history to the SERP. Due to our manually imposed history state, they’re actually taken to the #gref-less landing page.

Next, we check the dwell time on the page, using the difference between the current time and the time when the GTM container snippet was first loaded. This is a reasonable description of dwell time, but you can use something else for start time if you wish.

Finally, we check if oldFragment is false, meaning we’re still on the landing page. If it is, the payload is pushed into dataLayer. The ‘eventCallback’ key has the actual return to SERP command, and it will only be executed after any Tags that use the returnToSerp event have fired.

Hacky-dy-hack-hack! I love it!

All the other stuff you’ll need

Here’s a list of the assets you need to create for this to work. Feel free to improvise, if you wish!

1. Built-in Variables

First, make sure the following Built-in Variables are checked in your container’s Variables settings.

So that’s Page URL, Event, History Source, New / Old History Fragment.

2. The Triggers for the Custom HTML Tag

The Custom HTML Tag runs on two Triggers.

The first one is the default All Pages Trigger.

The second one is a History Change Trigger which looks like this:

This Trigger only launches when a browser history event is detected which is also a popstate, and the new history fragment is empty.

3. The Variables

Create the following Data Layer Variables:

This one stores the time when the GTM container snippet was executed.

This is where the time spent on the landing page is stored.

4. The Trigger for the Timing Tag

The Trigger you’ll attach to the Timing Tag (created next) looks like this:

This Trigger fires when the returnToSerp dataLayer event is pushed via the Custom HTML Tag. It also checks that the dwell time pushed into dataLayer is less than 30 minutes. This way a new session won’t be started with the event, if the visitor stays on the page for an exceptionally long time.

5. The Timing Tag

And here’s the Universal Analytics Timing Tag you’ll need:

We’re sending SERP Bounce as the Timing Category, the full Page URL as the Timing Variable, and the time spent on the landing page as the value. As mentioned above, this Tag is fired by the Trigger you created in step (4) above.

Viewing the results

Once you implement this, you’ll find the results under Site Content > Site Speed > User Timings under the Timing Category labelled SERP Bounce.

To drill into the data, it’s useful to view the Timing Variable as the primary dimension, as that’s the one where the landing pages are. Try to find landing pages with an abnormally small average dwell time:

These pages have a very short dwell time compared to site average. Just make sure that the timing sample is large enough for you to draw conclusions from. Pages with an abnormally short dwell time from the SERP MIGHT indicate a poor experience or lack or relevant content.

Also, the Distribution view is useful if you want to drill down to individual page performance:

This report lets you view the timing buckets. As you can see, there are some anomalies here, though the sample is quite small.

Summary

I hope I’ve convinced you by now that this is a very hacky solution. Remember to test thoroughly. I can also promise that you will have problems if you try to implement this on a single-page application (e.g. AJAX site), which rely on the browser history API for navigation.

Nevertheless, it’s an interesting way of uncovering potential issues with your landing pages. It would be interesting to align this data with keyword data, but that’s up to you to solve, since that data is difficult to come by these days, and User Timings don’t align well with acquisition dimensions like Keyword.