Scroll Tracking Plugin Example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Default Configuration | |
*/ | |
module.exports = { | |
action : 'Pageview End', | |
beacon : true, | |
category : 'Page', | |
debug : false, | |
delay : true, | |
labelNoScroll : 'Did Not Scroll', | |
labelScroll : 'Did Scroll', | |
sampleRate : 100, | |
scrollThreshold : 10, | |
setPage : true, | |
timeout : 300, | |
timeThreshold : 15, | |
metric : null, | |
maxTimeOnPage : 30 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* From: https://github.com/googleanalytics/autotrack | |
* | |
* Copyright 2016 Google Inc. All Rights Reserved. | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
var utilities = require('./utilities'); | |
/** | |
* Provides a plugin for use with analytics.js, accounting for the possibility | |
* that the global command queue has been renamed or not yet defined. | |
* @param {string} pluginName The plugin name identifier. | |
* @param {Function} pluginConstructor The plugin constructor function. | |
*/ | |
module.exports = function providePlugin(pluginName, pluginConstructor) { | |
var gaAlias = window['GoogleAnalyticsObject'] || 'ga'; | |
window[gaAlias] = window[gaAlias] || function() { | |
(window[gaAlias]['q'] = window[gaAlias]['q'] || []).push(arguments); | |
}; | |
// Formally provides the plugin for use with analytics.js. | |
window[gaAlias]('provide', pluginName, pluginConstructor); | |
// Registers the plugin on the global gaplugins object. | |
window.gaplugins = window.gaplugins || {}; | |
window.gaplugins[utilities.capitalize(pluginName)] = pluginConstructor; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* ScrollTracker | |
*/ | |
var utilities = require('./utilities') | |
var defaults = require('./defaults') | |
var provide = require('./provide') | |
function scrollDepthTracker(tracker, _config){ | |
var self = this | |
/** | |
* ga tracker object | |
* @type {object} | |
*/ | |
this.tracker = tracker | |
/** | |
* whether automated hit has been sent | |
* @type {Boolean} | |
*/ | |
this.autoHitSent = false | |
/** | |
* hit count | |
* @type {Number} | |
*/ | |
this.hitCount = 1 | |
/** | |
* configuration object | |
* @type {object} | |
*/ | |
this.config = utilities.extend(_config, defaults) | |
this.debug(this.config) | |
/** | |
* @type {Boolean} | |
*/ | |
this.hasScrolled = false | |
/** | |
* @type {Boolean} | |
*/ | |
this.everScrolled = false | |
/** | |
* maximum scroll reach recorded | |
* @type {Number} | |
*/ | |
this.reach = 0 | |
/** | |
* scroll reach on page load (usually window height) | |
* @type {Number} | |
*/ | |
this.initialReach = 0 | |
/** | |
* used to store the timestamp of DOMready/plugin initalization | |
*/ | |
this.startTime = new Date()*1 | |
/** | |
* Initalize plugin on DOMready | |
*/ | |
utilities.ready(function(){ | |
// event handler test | |
utilities.event(document)("click")(function(){ | |
document.innerHTML = "click!" + document.innerHTML | |
}) | |
// set inital reach to the scroll depth as soon as DOMReady occurs | |
self.initialReach = self.percent(self.depth(), self.pageHeight()) | |
if(self.config.setPage){ | |
if(self.config.setPage.big){ | |
self.tracker.set('page', self.config.setPage) | |
} else { | |
self.tracker.set('page', window.location.pathname) | |
} | |
} | |
self.debug("is mobile?", self.isMobile()) | |
// check if mobile device | |
if(self.isMobile() && document.visibilityState){ | |
// visiblity change event | |
utilities.event(document, self)('visibilitychange')(self.onVisibilityChange) | |
utilities.event(window, self)('pagehide')(self.onVisibilityChange) | |
} else { | |
// standard beforeunload event | |
utilities.event(window, self)('beforeunload')(self.onUnload) | |
} | |
// attach scroll event handler | |
utilities.event(window)('scroll')(function(){ | |
self.everScrolled = true | |
self.hasScrolled = true | |
}) | |
// sampled scroll | |
var sampleScroll = function(){ | |
if(self.hasScrolled){ | |
self.hasScrolled = false | |
self.onScroll() | |
} | |
setTimeout(sampleScroll, self.config.sampleRate) | |
} | |
setTimeout(sampleScroll, self.config.sampleRate) | |
var sampleTime = function(){ | |
self.checkTimeout() | |
setTimeout(sampleTime, 5000) | |
} | |
setTimeout(sampleTime, 5000) | |
}) | |
} | |
scrollDepthTracker.prototype.isMobile = function(){ | |
return window.navigator.userAgent.match(/Mobi|Touch|Opera\ Mini|Android/) | |
} | |
/** | |
* debug utility - only outputs to console if debug method is true | |
* @param {string} msg debug message | |
*/ | |
scrollDepthTracker.prototype.debug = function(){ | |
if(this.config.debug){ | |
console.log(utilities.toArray(arguments)) | |
} | |
} | |
/** | |
* calculate scroll depth to the bottom on the viewport | |
* @return {int} current scroll depth in pixels | |
*/ | |
scrollDepthTracker.prototype.depth = function(){ | |
return (window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop || 0) + | |
(window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight) | |
} | |
/** | |
* return percentage depth from depth and page height | |
* @param {integer} depth scroll depth in pixels | |
* @param {integer} height page height in pixels | |
* @return {integer} percent depth | |
*/ | |
scrollDepthTracker.prototype.percent = function(depth, height){ | |
return Math.floor(100 * depth / height) | |
} | |
/** | |
* check timeout | |
*/ | |
scrollDepthTracker.prototype.checkTimeout = function(){ | |
if( new Date()*1 - this.startTime > ((this.config.maxTimeOnPage -0.5) * 60 * 1000) && !this.autoHitSent ){ | |
this.autoHitSent = true | |
this.onUnload() | |
} | |
} | |
/** | |
* sampled scroll callback | |
* @return {null} | |
*/ | |
scrollDepthTracker.prototype.onScroll = function(){ | |
var self = this | |
var temp = this.percent(this.depth(), this.pageHeight()) | |
if(temp > this.reach){ | |
if(temp > 100){ | |
this.reach = 100 | |
} else { | |
this.reach = temp | |
} | |
} | |
if(this.config.metric && this.config.metric.toFixed){ | |
this.tracker.set('metric'+this.config.metric, this.reach) | |
} | |
this.debug('on scroll: reach', this.reach) | |
} | |
/** | |
* calculate height of document | |
* @return {int} height in pixels | |
*/ | |
scrollDepthTracker.prototype.pageHeight = function() | |
{ | |
return Math.max( | |
document.body.scrollHeight, | |
document.documentElement.scrollHeight, | |
document.body.offsetHeight, | |
document.documentElement.offsetHeight, | |
document.body.clientHeight, | |
document.documentElement.clientHeight | |
) | |
} | |
/** | |
* window beforeunload callback | |
* @return {null} | |
*/ | |
scrollDepthTracker.prototype.onUnload = function(){ | |
var self = this | |
if( new Date()*1 - this.startTime > ((this.config.maxTimeOnPage) * 60 * 1000)){ | |
return; | |
} | |
var nonInteraction = true | |
var skip = false | |
if(( new Date()*1 - this.startTime > this.config.timeThreshold * 1000) || (this.reach > this.initialReach + this.config.scrollThreshold)){ | |
nonInteraction = false | |
} | |
var action = this.config.action | |
if(this.hitCount > 1){ | |
action = action + " (" + this.hitCount + ")" | |
} | |
var data = { | |
eventCategory: this.config.category, | |
eventAction: action, | |
eventLabel: (this.everScrolled) ? this.config.labelScroll : this.config.labelNoScroll, | |
eventValue: this.reach, | |
nonInteraction: nonInteraction, | |
hitCallback: function(){ | |
skip = true | |
self.debug('hit sent!') | |
} | |
} | |
// check hit for debugging | |
this.debug(data) | |
// send event to GA | |
this.tracker.send('event', data) | |
this.hitCount++ | |
if (this.config.timeout && !window.navigator.sendBeacon){ | |
this.debug("no beacon support: using timeout fallback") | |
var start = new Date() | |
var run = 0 | |
do { | |
run = new Date() - start | |
} while (run < this.config.timeout && !skip) | |
this.debug('close page') | |
} | |
} | |
/** | |
* fires on visibility change event | |
* checks that it is changing to "hidden" and then fires normal onUnload event | |
* @return {[type]} [description] | |
*/ | |
scrollDepthTracker.prototype.onVisibilityChange = function(eventName){ | |
if(eventName === "pagehide"){ | |
this.onUnload() | |
} else if (eventName === "visibilitychange" && document.visibilityState == 'hidden') { | |
this.onUnload() | |
} | |
} | |
// provide plugin | |
provide('scrollDepthTracker', scrollDepthTracker) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Utilities | |
*/ | |
var Utilities = { | |
/** | |
* convert to Array | |
* @return {Array} | |
*/ | |
toArray : function toArray(arr){ | |
return Array.prototype.slice.call(arr) | |
}, | |
/** | |
* DOMready handler | |
* @param {function} fn handler | |
* @return {null} | |
*/ | |
ready : function ready(fn){ | |
if (document.readyState != 'loading') | |
{ | |
fn() | |
} else if (document.addEventListener){ | |
document.addEventListener('DOMContentLoaded', fn) | |
} else { | |
document.attachEvent('onreadystatechange', function () { | |
if (document.readyState != 'loading') fn() | |
}) | |
} | |
}, | |
/** | |
* Capitalizes the first letter of a string. | |
* @param {string} str The input string. | |
* @return {string} The capitalized string | |
*/ | |
capitalize: function(str) { | |
return str.charAt(0).toUpperCase() + str.slice(1); | |
}, | |
/** | |
* Object Extend Utility | |
* @return {Object} combined object | |
*/ | |
extend : function extend() | |
{ | |
var objects = Utilities.toArray(arguments) | |
var newObject = {} | |
var key | |
var i | |
function _extend(consumer, provider) | |
{ | |
var key | |
for(key in provider) | |
{ | |
if(!consumer.hasOwnProperty(key)){ | |
consumer[key] = provider[key] | |
} | |
} | |
return consumer | |
} | |
for(i in objects) | |
{ | |
objects[i] = _extend(newObject, objects[i]) | |
} | |
return newObject | |
}, | |
/** | |
* event handler utility | |
* @param {element} element DOM element to attach event listener | |
* @param {object} context The context the handler will be run in - can be overriden | |
* @return {function} Specify event | |
*/ | |
event : function event(element, context){ | |
// Assume modern browser | |
var pasteeater = false | |
// Check to see whether we're dealing with an old browser | |
// and change pasteeater to true if old browser detected | |
if(!element.addEventListener && element.attachEvent){ | |
pasteeater = true | |
} | |
/** | |
* specify event | |
* @param {string} event The event name | |
* @return {function} Add handler | |
*/ | |
return function(event){ | |
/** | |
* attach handler | |
* @param {function} handler event handler | |
* @param {object} data Any extra data to pass to the handler | |
* @param {object} _context Override the context the handler is run in | |
* @return {function} Function to remove event listener | |
*/ | |
return function(handler, data, _context){ | |
context = _context || context | |
if(!handler.call){ | |
throw Error('Callback is not a function') | |
} | |
// create new handler function based on arguments passed | |
var _handler = function(){ | |
handler.call(context || element, event, data) | |
} | |
// fallback for old browsers | |
if(pasteeater){ | |
element.attachEvent("on"+event, _handler) | |
} else { | |
element.addEventListener(event, _handler, false) | |
} | |
/** | |
* remove handler | |
* @return {function} Call this function to remove event listener | |
*/ | |
return function(){ | |
// fallback for old browsers | |
if(pasteeater){ | |
element.detachEvent(event, _handler) | |
} else { | |
element.removeEventListener(event, _handler, false) | |
} | |
} | |
} | |
} | |
} | |
} | |
module.exports = Utilities |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment