Skip to content

Instantly share code, notes, and snippets.

@chrisgoddard
Last active Jan 1, 2018
Embed
What would you like to do?
Scroll Tracking Plugin Example
/**
* 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
}
/**
* 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;
};
/**
* 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)
/**
* 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