Improve Your Bounce Rate with PageEngage for GTM

Ah, the age-old question, “What’s a good bounce rate?”

The answer is always the same.

“It depends.”

In this post, I want to clear up a common misconception about bounce rate in Google Analytics, discuss why the mechanics behind bounce calculation make for a flawed metric, and propose a method for making your bounce rate more relevant using a custom script called PageEngage that you can deploy through Google Tag Manager.

If you’re short on time, use the table of contents.

What is a “Bounce” Anyway?

I’d like to begin by talking about what a “bounce” actually refers to in the context of web analytics, specifically in the case of Google Analytics. The mental image that a term like “bounce rate” conjures up is a bit severe and possibly humorous — it makes me picture a halfling charging into a dwarven shield and just bouncing harmlessly off of it.

Let’s be clear: that’s not what’s happening to a user when Google Analytics reports a bounce. It’s not a user rejection and it’s not even a failure of your website. A bounce doesn’t even have to be viewed as a bad thing.

That is because a bounce, in the GA context, is simply a session with a single interaction. In Google’s own words, “a bounce is calculated specifically as a session that triggers only a single request to the Analytics server“.

There you have it: A bounce is a session with a single interaction. The value you assign to that type of session is up to you.

While a bounce at the top of your conversion funnel isn’t great (the one goal of that page was to get them to go deeper into the site), seeing a bounce on your 3,000-word blog post might be totally fine (if the goal was for them to read the post, all that’s required is a single interaction, i.e., a page view).

What’s a good bounce rate? It depends. Both on the type of page and on the type of website and that’s a decision for the team behind the site.

The Mechanics of a Bounce

I’d now like to talk about the mechanics of a bounce since that’s vital to understanding how we’re going to change it.

Think about the example I just gave of the 3,000-word blog post (we could even say that our example is this post. Very meta!). If someone reads our post and then leaves the site afterward, that session is recorded as a bounce. This is owed the mechanics of a bounce calculation Google Analytics. Think about Google’s quote one more time: “a bounce is calculated specifically as a session that triggers only a single request to the Analytics server”.

Quite a few of the metrics you see within Google Analytics are calculated using same two data points: the time that the session began (usually the initial page view, AKA entrance) and time that subsequent interactions on the site took place.

  • Time on Page: Time between entrance and navigation to another page
  • Time on Site: Time between entrance and last on-site interaction
  • Average Session Duration: Average of time on site for non-bounces
  • Bounce: Any visit in which only the time of entrance is known.

Can you see the issue? A bounce is just a single-interaction session! You might as well this of a bounce as “a session where we have limited data”.

If two users land on our 3,000-word post but one user spends 30 minutes digesting the whole piece and the other user immediately closes the browser tab, both sessions are counted as a bounce! 

What if the first scenario (in which a user spends a bunch of time reading the post) didn’t need to be counted as a bounce? That session would be accurately classified as an engagement session, while the other (which truly had no engagement or interaction) would still be a bounce. We could have some much more insightful data to work with.

What if we could make some changes and salvage bounce rate?

The last half of this post will talk about how to set that up; to negate bounces for engaged sessions so that your bounce rate can become more reflective of actual website performance and usage.

Note: An Analytics Bounce Isn’t SERP Pogosticking

Let’s quickly note that your bounce rate in Google Analytics has absolutely no bearing on your organic search ranking. I’ve been asked about this a few times recently and the asker has wanted to know if modifying your website’s bounce rate in Google Analytics (to show a lower percentage of bounces) can trick Google into thinking that your site should rank higher.

The answer is: No.

First, Google doesn’t use your Google Analytics data to determine your site’s organic rankings. They just don’t.

Second, what these folks are likely thinking of when they ask this question is the concept of SERP pogosticking. This is sometimes referred to as a bounce, but in the context of organic SERPs. It’s when a searcher clicks into your site but immediately heads back to the Google SERPs to refine their search — your site didn’t give them what they wanted and they could tell within seconds. That’s not a great quality signal!

This could be, and is, a whole blog post on its own: check out AJ Kohn’s Time to Long Click to learn about the differences between a “short click” and a “bounce”.

Moving on…

Negating Bounces: Modifying Your Bounce Rate in GA

So now that we know more about bounce rate, one course of action that might make sense (depending on your analytics goals and your site’s reason for existence) is to modify your bounce rate by negating bounces in special cases. We’d cancel out a bounce during sessions in which a user is fairly engaged but would otherwise slip under the radar as a single-interaction session.

Sticking with our 3,000-word blog post example, no one would argue that the person who opens the post but immediately closes the tab is a textbook bounce. But what about the person who read the whole thing and spent 10-20 minutes doing so? There’s a way in which we could tell Google Analytics to not count that as a bounce, but rather as an engaged multi-interaction user.

How to Negate Bounces

We’ll do that by sending an event hit (pageviews, events, and transaction are all “hits” that Google Analytics receives and are the basis for how metrics are calculated) to Google Analytics once we see that a user is truly engaged with a page. The cool thing about events is that you can fire them off at any time and create your own logic as to when that should happen.

A very specific example of this would be firing an event to Google Analytics once a specific website element came into view (e.g., the second image in a blog post’s content or the top of the comments section to signify scrolling to the end). It just so happens, you can do with Google Tag Manager’s great new Element Visibility Trigger. However, I want to propose getting a bit fancier with it than something like Element Visibility alone would allow.

Meet PageEngage, a Script for GTM

Rather than relying on one specific scenario to be the end-all-be-all of site engagement, I want to use a whole set of conditions. That way, we could listen for one of many things to happen or wait until a session had accumulated more than one before firing an event to Google Analytics.

To that end, I created PageEngage — a simple script that you can add to your Google Tag Manager account and negate bounces when users are engaged.

PageEngage monitors for three scenarios and once two of the three have occurred, it sends an event to Google Analytics which negates a bounce for that session. The scenarios are:

  1. The user selected some content and copied it to their clipboard
  2. The user scrolled all the way to the bottom of the page
  3. The user stayed on the page for X seconds

Of course, the possibilities are endless for something like this; these three are the first I thought of and could code up reasonably well. If you were so motivated and had some JavaScript skills, you could pretty easily write in your own custom scenarios.

How the Script Works

We’ll talk about how to implement this on your own site in the following section, but first, let’s talk about how this script works and what it does.

See the Pen Page Engage Script by Mike Arnesen (@mike_arnesen) on CodePen.

Feel free to open that up in a new tab so you can see it better. We’ll go through it block-by-block and line by line.


Block One: This is where we define variables. These will change depending on how you want to customize Page Engage.

[code language=”javascript”] var coolContent = document.querySelectorAll("h1,h2,p,li"); [/code]

The coolContent variable tells the script which HTML elements you care about. If you want to count people highlighting and copying any heading and paragraph, you can leave it as is. If your site uses <div> tags rather than <p> tags, you could add that to the end of the list.

[code language=”javascript”]
var qualityTime = 60;
[/code]

qualityTime allows you to specify how many seconds should pass before you consider a user engaged. This is later multiplied to convert it into milliseconds for JavaScript to work with so all you need to do is input the seconds.

[code language=”javascript”]
var threshold = 2;
[/code]

The threshold sets how many of these engagement events you want to see before sending the ultimate event hit to Google Analytics; the one that negates the bounce for that session. The default setting means that we’re waiting for two of the three scenarios to occur. Change it to your liking.


Block Two: This is where we track how much “engagement” has occurred. There’s a function counts how many times the user takes an action that we define as significant.

[code language=”javascript”]
var engagement = [];
[/code]

The engagement variable is an empty array. If that’s confusing language, just think of it as a place where we can store each interaction and count how many there are.

[code language=”javascript”]
function plusOneEngagement(interaction) {
engagement.push(interaction);
}
[/code]

A function is a tool built for a specific purpose that you can use over and over again (like a hammer, a drill, a level, etc.). This tool, called plusOneEngagement(), simply adds an interaction — like a “scroll”, “copy”, or “time” — to our engagement array every time we bring it out.


Block Three: This is a function that allows us to check the number of “engagements” and, once we hit the magic number, we push a custom event to the dataLayer (which will use to send an Event hit to GA using Google Tag Manager).

[code language=”javascript”]
function pageEngaged() {
if (engagement.length == threshold) {
console.log("Page engaged!");
dataLayer.push({ event: "pageEngaged" });
}
}
[/code]

What you see above is another tool in our toolbelt, a function called pageEngaged(). Its main components are broken out below.

[code language=”javascript”]
if (engagement.length == threshold)
[/code]

We count how many interactions have been pushed into the engagement array variable and if that number matches what we’ve set for the threshold (i.e., 2) we execute the next two lines.

[code language=”javascript”]
console.log("Page engaged!");
dataLayer.push({ event: "pageEngaged" })
[/code]

The first line logs a console message for debugging purposes. This serves no purpose other than to aid advanced users in troubleshooting. The second line, however, is the most critical piece. It pushes a custom event to the dataLayer — another JavaScript array object that Google Tag Manager is built to listen to constantly. In the implementation section, I’ll talk about setting GTM to fire off our GA event when it hears the “pageEngaged” event get pushed into the dataLayer.


Block Four: Defines how we listen for someone copying a piece of content and subsequently logs that interaction.

[code language=”javascript”]
for (var i = 0; i &amp;amp;amp;amp;lt; coolContent.length; i++) {
coolContent[i].addEventListener("copy", function() {
plusOneEngagement("copied content");
pageEngaged();
});
}
[/code]

That’s the block in its entirety, but let’s dive right into the components.

[code language=”javascript”]
for (var i = 0; i &amp;amp;amp;amp;lt; coolContent.length; i++)
[/code]

This is a standard JavaScript “for loop”. The first part sets up the i variable, which is a glorified counter. It starts at 0. The second part tells the loop how long to run — for as long as i is less than the number of entries in coolContent (which is four because we have h1,h2,p,li specified). The third and final part declares that after each loop through, we add 1 to i (in technical terms, we “increment” the variable).

[code language=”javascript”]
coolContent[i].addEventListener("copy", function() {

});
[/code]

This is the super-powered portion of the for loop. coolContent[i] lets us go through all of our content elements in succession. The changing value of i lets us work with the first content type (all the <h1> tags) on the first pass, then all the <h2> tags on the second pass, and so on. On each pass, we add an EventListener that listens for when that element is copied by the user. The final function() provides the action we want JavaScript to take when that Event is heard. See what that action is below.

[code language=”javascript”]
plusOneEngagement("copied content");
[/code]

This is the first time we’re calling our plusOneEngagement() function! It’s so much fun pulling our tools out of the toolbox. Since we’ve included “copied content” inside the parenthesis, that word (i.e. string) gets passed into the function and, ultimately, the engagement array.

[code language=”javascript”]
pageEngaged();
[/code]

Finally, we call on another function. Unlike the previous line, we don’t need to pass any values into the pageEngaged() function so the parentheses are empty.


Block Five: Defines some basic scroll tracking and subsequently logs that interaction.

[code language=”javascript”]
window.addEventListener("scroll", deepScroll);
function deepScroll(ev) {
if (window.innerHeight + window.pageYOffset &amp;amp;amp;amp;gt;= document.body.offsetHeight) {
plusOneEngagement("scrolled deep");
pageEngaged();
window.removeEventListener("scroll", deepScroll);
}
}
[/code]

This is a big one, but it’s not a scary as it looks. Let’s break down the components.

[code language=”javascript”]
window.addEventListener("scroll", deepScroll);
[/code]

The first thing we do is add another EventListener, but this time we don’t have to attach it to multiple content elements. We only have to attach it to one; the browser “window”. When a scroll is detected, we call a function called deepScroll(),  which we’ll discuss next.

[code language=”javascript”]
function deepScroll(ev)
[/code]

This line creates the function and tells it to use the event that’s been detected, ev.

[code language=”javascript”] if (window.innerHeight + window.pageYOffset &amp;amp;amp;amp;gt;= document.body.offsetHeight) { [/code]

But we don’t care about just any scroll; we only care about a deep scroll down the page. If the window’s innerHeight plus its pageYOffset is greater than the body’s offsetHeight, we know it’s a solid interaction and we move along in the code (apologies for the barrage of JS jargon; it’s too much to explain all of that in this post).

[code language=”javascript”]
plusOneEngagement("scrolled deep");
[/code]

We call our plusOneEngagement() function here as well. Since we’ve included “scrolled deep” inside the parenthesis, that word (i.e. string) gets passed into the function and, ultimately, the engagement array.

[code language=”javascript”]
pageEngaged();
[/code]

We then call the pageEngaged() function.

[code language=”javascript”]
window.removeEventListener("scroll", deepScroll);
[/code]

Unlike Block Four, at the end of this block, we actually want to remove the scroll listener. Once they’ve scrolled deep once, we don’t need to have JavaScript continue to check for it.


Block Six: Sets a countdown for X seconds and subsequently logs that interaction.

[code language=”javascript”]
setTimeout(function() {
console.log("Passed time");
plusOneEngagement("passed time");
pageEngaged();
}, qualityTime * 1000);
[/code]

This whole thing sets a timer once the site loads and logs an interaction once enough time has passed. Let’s take a look at its components.

[code language=”javascript”]
setTimeout(function() {

}, qualityTime * 1000);
[/code]

setTimeout is a JavaScript method for taking some action after a specified amount of time. First, we define what should happen once the time in up. Second, we tell it how many milliseconds the timer should run for. Since we specified whole seconds at the very beginning, we multiply by 1,000 to get ms.

[code language=”javascript”]
plusOneEngagement("passed time");
[/code]

Again, we call our plusOneEngagement() function. Since we’ve included “passed time” inside the parenthesis, that word (i.e. string) gets passed into the function and, ultimately, the engagement array.

[code language=”javascript”]
pageEngaged();
[/code]

We then call the pageEngaged() function.

Implementation in GTM

Now that we’ve broken down how the PageEngage script works, it’s time to implement in Google Tag Manager. We’ll be creating two new tags, one new trigger, and one new variable. Plus, we’ll leverage one variable and one trigger that you (almost certainly) already have.

Tag #1: PageEngage Script

Create a new tag in GTM and select the Custom HMTL tag type. Copy the PageEngage script here and paste that into the tag. Remember: the whole thing must be wrapped in <script> tags.

Trigger (Pre-existing): All Pages

All GTM containers have this one by default, so select it as the firing condition for the tag you just created. If you only want PageEnageg to negate bounce rate on certain types of pages (just blog posts), you can create a new trigger for that purpose.

Trigger #1: pageEngaged Custom Event

Next, we’ll create a new trigger that listens for the PageEngage script’s dataLayer push. To do that, we create a new trigger in GTM using the “Custom Event” trigger type. This allows us to specify a custom dataLayer event to listen for “pageEngaged”.

This trigger will be used to fire the Google Analytics event tag a few steps down in this post.

Variable #1: engagedActions

Create a new variable using the Data Layer variable type. All you need to do is specify the “Data Layer Variable Name” as “engagedActions” and GTM will be able to grab the value of “engagedActions” as generated by the PageEngage script. That way, we can capture this data to send with our Google Analytics Event tag (up next!).

Tag #2: PageEngage Event

To top it all off create another tag, but this time chose the “Universal Analytics Google Analytics” tag type. Change the Track Type to “Event”. Then, use the following values for the event data:

  • Category: pageEngage
  • Action: engaged session
  • Label: {{engagedAction}} — This is a reference to the dataLayer variable we created in the previous step.

Finally, we have to specify a Google Analytics Settings Variable so that GTM knows where to send the data to.

Variable (Pre-existing): GA Settings

If you’re already using GTM, this variable should already exist in one form or another. You might see it named as something other than “GA Settings” but it’ll be a Google Analytics Settings variable. Use the one you already have rather than creating a new one.

 Test, Launch, and Get Better Data

As with everything you do with Google Tag Manager, be sure to test this out in Preview Mode first to confirm that everything is working as expected. Comment on this post if you get stuck. Once you know that everything is working properly, Publish and watch your bounce rate become much more valuable.

A word of warning: if you decide to implement, you’ll almost certainly see your site’s bounce rate plummet in the days that follow. This is normal and will set the new baseline that you’re going to want to improve against.

To close, let me say that negating bounces to modify your bounce rate isn’t the right move for every person and every site. As with so many things in our world of optimization, the answer to whether or not you should do something is usually, “It depends”. But, if you decide this is the right move for you, I think you’ll be happy with and your bounce data will be much more reflective of your site’s actual performance.

Until next time. Happy optimizing!

Written by
Mike founded UpBuild in 2015 and served as its CEO for seven years, before passing the torch to Ruth Burr Reedy. Mike remains with the company today as Head of Business Operations.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *