Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Snippet to track load abandonments (bounces) #1355

Closed
chrisblakley opened this issue Jun 1, 2017 · 16 comments
Closed

Snippet to track load abandonments (bounces) #1355

chrisblakley opened this issue Jun 1, 2017 · 16 comments
Labels
Frontend (Script) Related to the client-side JavaScript. 💭 Question / Research Additional research/discussion is needed to answer this question.
Milestone

Comments

@chrisblakley
Copy link
Owner

chrisblakley commented Jun 1, 2017

This is from Google I/O 2017 that I watched, but didn't catch the abandonment tracking part...
https://developers.google.com/web/updates/2017/06/user-centric-performance-metrics

This snippet tracks load abandonments in GA. Research and test it quite a bit more before we implement it.

window.__trackAbandons = () => {
  // Remove the listener so it only runs once.
  document.removeEventListener('visibilitychange', window.__trackAbandons);
  const ANALYTICS_URL = 'https://www.google-analytics.com/collect';
  const GA_COOKIE = document.cookie.replace(
    /(?:(?:^|.*;)\s*_ga\s*\=\s*(?:\w+\.\d\.)([^;]*).*$)|^.*$/, '$1');
  const TRACKING_ID = 'UA-21292978-3'; //@todo: Change tracking ID here
  const CLIENT_ID =  GA_COOKIE || (Math.random() * Math.pow(2, 52));

  // Send the data to Google Analytics via the Measurement Protocol.
  navigator.sendBeacon && navigator.sendBeacon(ANALYTICS_URL, [
    'v=1',
    't=event',
    'ec=Load',
    'ea=abandon',
    'ni=1',
    'tid=' + TRACKING_ID,
    'cid=' + CLIENT_ID,
    'ev=' + Math.round(performance.now()),
  ].join('&'));
};
document.addEventListener('visibilitychange', window.__trackAbandons);

Remove the listener once the page becomes interactive:
document.removeEventListener('visibilitychange', window.__trackAbandons);

Make sure that this does not conflict with Autotrack's pagevisibility tracking or if it is already implemented within that library.

@chrisblakley chrisblakley added Frontend (Script) Related to the client-side JavaScript. 💭 Question / Research Additional research/discussion is needed to answer this question. labels Jun 1, 2017
@chrisblakley chrisblakley added this to the 6.0 Ant milestone Jun 1, 2017
@chrisblakley chrisblakley changed the title Snippet to track load abandonments Snippet to track load abandonments (bounces) Jun 3, 2017
@chrisblakley
Copy link
Owner Author

Here's what I ended up with. Testing on Nebula Documentation site only:

add_action('nebula_head_open', 'ga_track_load_abandons');
function ga_track_load_abandons(){
	?>
	<script>
		window.__trackAbandons = () => {
			document.removeEventListener('visibilitychange', window.__trackAbandons); //Remove the listener so it only runs once.

			//Send the data to Google Analytics via the Measurement Protocol.
			navigator.sendBeacon && navigator.sendBeacon('https://www.google-analytics.com/collect', [
				'v=1',
				't=event',
				'ec=Load',
				'ea=abandon',
				'ni=1',
				'tid=<?php echo nebula()->get_option('ga_tracking_id'); ?>',
				'cid=' + document.cookie.replace(/(?:(?:^|.*;)\s*_ga\s*\=\s*(?:\w+\.\d\.)([^;]*).*$)|^.*$/, '$1') || (Math.random() * Math.pow(2, 52)),
				'ev=' + Math.round(performance.now()),
			].join('&'));
		};
		document.addEventListener('visibilitychange', window.__trackAbandons);

		//Remove the load abandon tracker on window load
		window.onload = function(){
			document.removeEventListener('visibilitychange', window.__trackAbandons);
		};
	</script>
	<?php
}

I have some questions/concerns before shipping this to production about how it affects other reports since we're sending an event before/without a pageview.

@chrisblakley
Copy link
Owner Author

Just noting that this event does not have contextual information that comes with a pageview. I've updated the payload to include more info, but it will never be as effective as a true pageview. I'm sure this will affect other reports.

I've also sent a second payload as a User Timing. This is a sampled report, so it won't show a true number of load abandons, but could provide some more information.

@chrisblakley
Copy link
Owner Author

Updated again. This code now sends the event with a label if GA is ready or not.

<script>
	window.__trackAbandons = () => {
		document.removeEventListener('visibilitychange', window.__trackAbandons); //Remove the listener so it only runs once.

		//https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters

		//Event
		//This could mess with other reports as an event could be sent before/without a pageview
		loadAbandonLabel = 'Before GA was ready';
		if ( window.GAready ){
			loadAbandonLabel = 'After GA was ready';
		}

		navigator.sendBeacon && navigator.sendBeacon('https://www.google-analytics.com/collect', [
			'tid=<?php echo nebula()->get_option('ga_tracking_id'); ?>', //Tracking ID
			'cid=' + document.cookie.replace(/(?:(?:^|.*;)\s*_ga\s*\=\s*(?:\w+\.\d\.)([^;]*).*$)|^.*$/, '$1') || (Math.random() * Math.pow(2, 52)), //Client ID
			'v=1', //Protocol Version
			't=event', //Hit Type
			'ec=Load', //Event Category
			'ea=abandon', //Event Action
			'el=' + loadAbandonLabel + ' (and before window load)', //Event Label
			'ev=' + Math.round(performance.now()), //Event Value
			'ni=1', //Non-Interaction Hit
			'dr=<?php echo ( isset($_SERVER['HTTP_REFERER']) )? $_SERVER['HTTP_REFERER'] : ''; ?>', //Document Referrer
			'dl=' + window.location.href, //Document Location URL
			'dt=' + document.title, //Document Title
		].join('&'));

		//User Timing
		//These are sampled, so it might not provide an accurate number of abandons
		navigator.sendBeacon && navigator.sendBeacon('https://www.google-analytics.com/collect', [
			'tid=<?php echo nebula()->get_option('ga_tracking_id'); ?>', //Tracking ID
			'cid=' + document.cookie.replace(/(?:(?:^|.*;)\s*_ga\s*\=\s*(?:\w+\.\d\.)([^;]*).*$)|^.*$/, '$1') || (Math.random() * Math.pow(2, 52)), //Client ID
			'v=1', //Protocol Version
			't=timing', //Hit Type
			'utc=Load Abandon', //Timing Category
			'utv=Abandon', //Timing Variable Name
			'utt=' + Math.round(performance.now()), //Timing Time (milliseconds)
			'utl=From%20navigation%20start%20until%20abandonment', //Timing Label
		].join('&'));
	};
	document.addEventListener('visibilitychange', window.__trackAbandons);

	//Remove the load abandon tracker on window load
	window.onload = function(){
		document.removeEventListener('visibilitychange', window.__trackAbandons);
	};

/*
	document.addEventListener('gaready', function(){
		document.removeEventListener('visibilitychange', window.__trackAbandons);
	});
*/
</script>

Added a raw JS event trigger in analytics.php that we can listen to for when GA is ready (to supplement the window var). Not using it (see commented out above), but could consider.

@chrisblakley
Copy link
Owner Author

Might be cool to try all bounces regardless of onload by "eavesdropping" on Google Analytics data using tasks and sending this bounce payload until an interaction event or second pageview is triggered?

https://developers.google.com/analytics/devguides/collection/analyticsjs/tasks

Here's a snippet to start:

ga('set', 'sendHitTask', function(model) {
  console.log(model.get('hitPayload'));
});

Not sure what data is available in individual payloads- like, if we can even tell if the user is a bounce. Interactive vs. Non-Interactive events would be easy, but I don't want to get crazy complicated needing to set my own cookie for multiple pageviews.

@chrisblakley
Copy link
Owner Author

Updated again to include beforeunload events. It also passes the event type to the GA action:

window.__trackAbandons = (e) => {
	document.removeEventListener('visibilitychange', window.__trackAbandons); //Remove the listeners so it only runs once.
	document.removeEventListener('beforeunload', window.__trackAbandons); //Remove the listeners so it only runs once.

	//Event
	//This could mess with other reports as an event could be sent before/without a pageview
	loadAbandonLabel = 'Before GA was ready';
	if ( window.GAready ){
		loadAbandonLabel = 'After GA was ready';
	}

	//https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters

	navigator.sendBeacon && navigator.sendBeacon('https://www.google-analytics.com/collect', [
		'tid=<?php echo nebula()->get_option('ga_tracking_id'); ?>', //Tracking ID
		'cid=' + document.cookie.replace(/(?:(?:^|.*;)\s*_ga\s*\=\s*(?:\w+\.\d\.)([^;]*).*$)|^.*$/, '$1') || (Math.random() * Math.pow(2, 52)), //Client ID
		'v=1', //Protocol Version
		't=event', //Hit Type
		'ec=Load', //Event Category
		'ea=Abandon+(' + e.type + ')', //Event Action
		'el=' + loadAbandonLabel + ' (and before window load)', //Event Label
		'ev=' + Math.round(performance.now()), //Event Value
		'ni=1', //Non-Interaction Hit
		'dr=<?php echo ( isset($_SERVER['HTTP_REFERER']) )? $_SERVER['HTTP_REFERER'] : ''; ?>', //Document Referrer
		'dl=' + window.location.href, //Document Location URL
		'dt=' + document.title, //Document Title
	].join('&'));

	//User Timing
	//These are sampled, so it might not provide an accurate number of abandons
	navigator.sendBeacon && navigator.sendBeacon('https://www.google-analytics.com/collect', [
		'tid=<?php echo nebula()->get_option('ga_tracking_id'); ?>', //Tracking ID
		'cid=' + document.cookie.replace(/(?:(?:^|.*;)\s*_ga\s*\=\s*(?:\w+\.\d\.)([^;]*).*$)|^.*$/, '$1') || (Math.random() * Math.pow(2, 52)), //Client ID
		'v=1', //Protocol Version
		't=timing', //Hit Type
		'utc=Load Abandon', //Timing Category
		'utv=Abandon', //Timing Variable Name
		'utt=' + Math.round(performance.now()), //Timing Time (milliseconds)
		'utl=' + loadAbandonLabel + ' (and before window load)', //Timing Label
		'dl=' + window.location.href, //Document Location URL
		'dt=' + document.title, //Document Title
	].join('&'));
};
document.addEventListener('visibilitychange', window.__trackAbandons);
document.addEventListener('beforeunload', window.__trackAbandons);

//Remove the load abandon tracker on window load
window.onload = function(){
	document.removeEventListener('visibilitychange', window.__trackAbandons);
	document.removeEventListener('beforeunload', window.__trackAbandons);
};

@chrisblakley
Copy link
Owner Author

chrisblakley commented Jun 13, 2017

Still sort of feeling around on this, but I've updated it again to include "Hard" (unload) or "Soft" (changing tab/window) abandonments.

window.__trackAbandons = (e) => {
	//Remove the listeners so it only runs once.
	document.removeEventListener('visibilitychange', window.__trackAbandons);
	document.removeEventListener('beforeunload', window.__trackAbandons);
	document.removeEventListener('unload', window.__trackAbandons);

	//Event
	//This could mess with other reports as an event could be sent before/without a pageview

	loadAbandonLevel = 'Hard (Unload)';
	if ( e.type == 'visibilitychange' ){
		loadAbandonLevel = 'Soft (Visibility Change)';
	}

	loadAbandonLabel = 'Before GA was ready';
	if ( window.GAready ){
		loadAbandonLabel = 'After GA was ready';
	}

	//https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters

	navigator.sendBeacon && navigator.sendBeacon('https://www.google-analytics.com/collect', [
		'tid=<?php echo nebula()->get_option('ga_tracking_id'); ?>', //Tracking ID
		'cid=' + document.cookie.replace(/(?:(?:^|.*;)\s*_ga\s*\=\s*(?:\w+\.\d\.)([^;]*).*$)|^.*$/, '$1') || (Math.random() * Math.pow(2, 52)), //Client ID
		'v=1', //Protocol Version
		't=event', //Hit Type
		'ec=Load Abandon', //Event Category
		'ea=' + loadAbandonLevel, //Event Action
		'el=' + loadAbandonLabel + ' (and before window load)', //Event Label
		'ev=' + Math.round(performance.now()), //Event Value
		'ni=1', //Non-Interaction Hit
		'dr=<?php echo ( isset($_SERVER['HTTP_REFERER']) )? $_SERVER['HTTP_REFERER'] : ''; ?>', //Document Referrer
		'dl=' + window.location.href, //Document Location URL
		'dt=' + document.title, //Document Title
	].join('&'));

	//User Timing
	//These are sampled, so it might not provide an accurate number of abandons
	navigator.sendBeacon && navigator.sendBeacon('https://www.google-analytics.com/collect', [
		'tid=<?php echo nebula()->get_option('ga_tracking_id'); ?>', //Tracking ID
		'cid=' + document.cookie.replace(/(?:(?:^|.*;)\s*_ga\s*\=\s*(?:\w+\.\d\.)([^;]*).*$)|^.*$/, '$1') || (Math.random() * Math.pow(2, 52)), //Client ID
		'v=1', //Protocol Version
		't=timing', //Hit Type
		'utc=Load Abandon', //Timing Category
		'utv=' + loadAbandonLevel, //Timing Variable Name
		'utt=' + Math.round(performance.now()), //Timing Time (milliseconds)
		'utl=' + loadAbandonLabel + ' (and before window load)', //Timing Label
		'dl=' + window.location.href, //Document Location URL
		'dt=' + document.title, //Document Title
	].join('&'));
};
document.addEventListener('visibilitychange', window.__trackAbandons);
document.addEventListener('beforeunload', window.__trackAbandons);
document.addEventListener('unload', window.__trackAbandons);

//Remove the load abandon tracker on window load
window.onload = function(){
	document.removeEventListener('visibilitychange', window.__trackAbandons);
	document.removeEventListener('beforeunload', window.__trackAbandons);
	document.removeEventListener('unload', window.__trackAbandons);
};

I don't know if the GAready check is really essential for this, but for now I'm leaving it in. Also, when it comes time to implement, I think choosing between the GA Timing or the Event will need to happen. Leaning towards the event at this point.

@chrisblakley
Copy link
Owner Author

chrisblakley commented Jun 13, 2017

Note: This will live in the /libs/Scripts.php class and trigger on nebula_head_open which is the highest anything can possibly be in the <head> of the document.

@chrisblakley
Copy link
Owner Author

chrisblakley commented Jun 14, 2017

Here's the final script. Reworked to be easier to read and for some better performance (and removed some unnecessary bits).

I placed this in the class /libs/Utilities/Analytics.php instead of scripts. Essentially it doesn't need to live in a class at all, but I feel better about it being there than in the the /inc/metadata.php file (where the hook places it).

document.addEventListener("visibilitychange", newAbandonTracker);
window.onbeforeunload = newAbandonTracker;

function newAbandonTracker(e){
	if ( e.type == 'visibilitychange' && document.visibilityState == 'visible' ){
		return false;
	}
	
	//Remove listeners so this can only trigger once
	document.removeEventListener("visibilitychange", newAbandonTracker);
	window.onbeforeunload = null;

	loadAbandonLevel = 'Hard (Unload)';
	if ( e.type == 'visibilitychange' ){
		loadAbandonLevel = 'Soft (Visibility Change)';
	}

	navigator.sendBeacon && navigator.sendBeacon('https://www.google-analytics.com/collect', [
		'tid=<?php echo nebula()->get_option('ga_tracking_id'); ?>', //Tracking ID
		'cid=' + document.cookie.replace(/(?:(?:^|.*;)\s*_ga\s*\=\s*(?:\w+\.\d\.)([^;]*).*$)|^.*$/, '$1') || (Math.random()*Math.pow(2, 52)), //Client ID
		'v=1', //Protocol Version
		't=event', //Hit Type
		'ec=Load Abandon', //Event Category
		'ea=' + loadAbandonLevel, //Event Action
		'el=Before window load', //Event Label
		'ev=' + Math.round(performance.now()), //Event Value
		'ni=1', //Non-Interaction Hit
		'dr=<?php echo ( isset($_SERVER['HTTP_REFERER']) )? $_SERVER['HTTP_REFERER'] : ''; ?>', //Document Referrer
		'dl=' + window.location.href, //Document Location URL
		'dt=' + document.title, //Document Title
	].join('&'));

	//User Timing
	navigator.sendBeacon && navigator.sendBeacon('https://www.google-analytics.com/collect', [
		'tid=<?php echo nebula()->get_option('ga_tracking_id'); ?>', //Tracking ID
		'cid=' + document.cookie.replace(/(?:(?:^|.*;)\s*_ga\s*\=\s*(?:\w+\.\d\.)([^;]*).*$)|^.*$/, '$1') || (Math.random()*Math.pow(2, 52)), //Client ID
		'v=1', //Protocol Version
		't=timing', //Hit Type
		'utc=Load Abandon', //Timing Category
		'utv=' + loadAbandonLevel, //Timing Variable Name
		'utt=' + Math.round(performance.now()), //Timing Time (milliseconds)
		'utl=Before window load', //Timing Label
		'dl=' + window.location.href, //Document Location URL
		'dt=' + document.title, //Document Title
	].join('&'));
}

//Remove abandonment listeners on window load
window.addEventListener('load', function(){
	document.removeEventListener('visibilitychange', loadAbandonTracking);
	if ( window.onbeforeunload === loadAbandonTracking ){
		window.onbeforeunload = null;
	}
});

Events vs. Timings:

I decided to keep both the event and the timing. Eventually when I'm convinced one way or another I can remove one. Some thoughts on these:

  • Events are not sampled as strictly as timings, so the total/unique numbers are more accurate
  • Timings report show the actual times better in seconds and has a total column.
    • Event total value is useless as it adds all times together, so Avg. Value needs to be used (as ms)
  • There is no way to view individual values for neither events or timings, so outliers will heavily influence average value.
    • May want to append the labels with individual timing... Not in love with this idea.
      • Other options include custom dimension or metric.
      • I wish there was a way to get median, average, min, and max within Google Analytics (I've heard this is available with BigQuery).
  • Events have better contextual data. Especially if the user has a CID already (or if the GA script gets loaded quick enough)
  • Not sure how relevant, but events can be used for goals, segments, and report filters. Timings aren't as flexible.

@chrisblakley
Copy link
Owner Author

Note: This does not work in Safari or any IE (it does work in Edge), so while the total number of load abandons will not be 100% accurate, the timings will be. Eventually the total abandons accuracy will increase as browser support grows and usage of old browsers dies.

https://caniuse.com/#search=beacon

@chrisblakley
Copy link
Owner Author

chrisblakley commented Jun 14, 2017

Retrofitting

To retrofit into old procedural sites, use the following PHP snippet. I put this in /functions/nebula_utilities.php near the other Google Analytics stuff:

(Note: This has been edited to include the visibility check from further below)

//Load abandonment tracking
add_action('nebula_head_open', 'ga_track_load_abandons'); //This is the earliest anything can be placed in the <head>
function ga_track_load_abandons(){
	if ( nebula_is_bot() ){ //This may not exist on really old sites
		return false;
	}	
?>
	<script>
		document.addEventListener('visibilitychange', loadAbandonTracking);
		window.onbeforeunload = loadAbandonTracking;
		
		function loadAbandonTracking(e){
			if ( e.type == 'visibilitychange' && document.visibilityState == 'visible' ){
				return false;
			}
		
			//Remove listeners so this can only trigger once
			document.removeEventListener('visibilitychange', loadAbandonTracking);
			window.onbeforeunload = null;
		
			loadAbandonLevel = 'Hard (Unload)';
			if ( e.type == 'visibilitychange' ){
				loadAbandonLevel = 'Soft (Visibility Change)';
			}
		
			//Grab the Google Analytics CID from the cookie (if it exists)
			gaCID = document.cookie.replace(/(?:(?:^|.*;)\s*_ga\s*\=\s*(?:\w+\.\d\.)([^;]*).*$)|^.*$/, '$1');
			newReturning = 'Returning visitor or multiple pageview session';
			if ( !gaCID ){
				gaCID = (Math.random()*Math.pow(2, 52));
				newReturning = 'New user or blocking Google Analytics cookie';
			}
		
			navigator.sendBeacon && navigator.sendBeacon('https://www.google-analytics.com/collect', [
				'tid=<?php echo nebula_option('ga_tracking_id'); ?>', //Tracking ID
				'cid=' + gaCID, //Client ID
				'v=1', //Protocol Version
				't=event', //Hit Type
				'ec=Load Abandon', //Event Category
				'ea=' + loadAbandonLevel, //Event Action
				'el=' + newReturning, //Event Label
				'ev=' + Math.round(performance.now()), //Event Value
				'ni=1', //Non-Interaction Hit
				'dr=<?php echo ( isset($_SERVER['HTTP_REFERER']) )? $_SERVER['HTTP_REFERER'] : ''; ?>', //Document Referrer
				'dl=' + window.location.href, //Document Location URL
				'dt=' + document.title, //Document Title
			].join('&'));
		
			//User Timing
			navigator.sendBeacon && navigator.sendBeacon('https://www.google-analytics.com/collect', [
				'tid=<?php echo nebula_option('ga_tracking_id'); ?>', //Tracking ID
				'cid=' + gaCID, //Client ID
				'v=1', //Protocol Version
				't=timing', //Hit Type
				'utc=Load Abandon', //Timing Category
				'utv=' + loadAbandonLevel, //Timing Variable Name
				'utt=' + Math.round(performance.now()), //Timing Time (milliseconds)
				'utl=' + newReturning, //Timing Label
				'dl=' + window.location.href, //Document Location URL
				'dt=' + document.title, //Document Title
			].join('&'));
		}
		
		//Remove abandonment listeners on window load
		window.addEventListener('load', function(){
			document.removeEventListener('visibilitychange', loadAbandonTracking);
			if ( window.onbeforeunload === loadAbandonTracking ){
				window.onbeforeunload = null;
			}
		});
	</script>
	<?php
}

@chrisblakley
Copy link
Owner Author

Another thing I was just thinking about is if the user opens it in a new tab and then changes to that tab while it is still loading, it will trigger a false-positive abandonment.

I should look into checking what type of visibility change happened to make sure it is changing from visible to hidden before sending the event.

@chrisblakley
Copy link
Owner Author

chrisblakley commented Jun 14, 2017

Ok, added this to the top of the function:

if ( e.type == 'visibilitychange' && document.visibilityState == 'visible' ){
	return false;
}

I also added a return false to prevent bots from being tracked (Note: this is PHP not JS).

if ( $this->is_bot() ){
	return false;
}

@chrisblakley
Copy link
Owner Author

Seeing this on one live site now... Weird that typical loading time is 6-8 seconds, but the abandoned loads are reporting as 50-90 seconds... I'm not seeing this on other sites, but it seems oddly coincidental that it's off by a factor of 10 on this one...

screen shot 2017-06-14 at 2 28 05 pm

@chrisblakley
Copy link
Owner Author

That discrepancy was due to a race condition on window load. jQuery was overriding the previous function, so the abandonment event listeners were never removed. I've updated Nebula and will update the retrofit function above, too.

@chrisblakley
Copy link
Owner Author

I added a check for new vs. returning visitor (or multipage session). Not going to update any of the previous snippets except for the retrofit one now because I'm sure I'll be making little tweaks like this constantly.

@chrisblakley
Copy link
Owner Author

As a note- with the new Hit ID dimension we're able to get median data in Google Analytics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Frontend (Script) Related to the client-side JavaScript. 💭 Question / Research Additional research/discussion is needed to answer this question.
Projects
None yet
Development

No branches or pull requests

1 participant