├── .gitignore
├── README.md
├── examples
└── keen-io.html
├── package.json
├── screentime.js
└── test
└── index.html
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Screentime
2 | ==========
3 |
4 | [Screentime](http://screentime.parsnip.io/) is a small tool that helps you start thinking of your website traffic in terms of time instead of hits (Pageviews, visits, etc). You can define areas of the page, called Fields, and then Screentime will keep track of how much time each Field is on screen for. You can also use it to track smaller elements, like ad units.
5 |
6 | Screentime only handles the client side work. You'll need to provide your own backend to post the data to. I've included an example that shows how to this with [Keen IO](https://keen.io/) using only a few lines of code. There's also a built-in option for posting to Google Analytics but there are some caveats (see below).
7 |
8 | [**View the Project Page**](http://screentime.parsnip.io/)
9 |
10 | ## How it works
11 | You specify some DOM elements that you want to track and then every second Screentime checks the viewport to see which ones are in view. It tracks the viewable seconds for each element/field and then issues a report every 10 seconds (you can adjust the interval). The report is passed to a callback function that you can use to post the data to your server.
12 |
13 | If the user switches tabs, the timer stops (using Addy Osmani's [Page Visibility polyfill](https://github.com/addyosmani/visibly.js)). The timer doesn't require that the user be active, just that the screen elements are visible.
14 |
15 | ## Usage
16 | jQuery is required. Pass in selectors for each **unique element** you want to track, including a name for each. The callback option receives an object containing the data.
17 |
18 | ```javascript
19 | $.screentime({
20 | fields: [
21 | { selector: '#top',
22 | name: 'Top'
23 | },
24 | { selector: '#middle',
25 | name: 'Middle'
26 | },
27 | { selector: '#bottom',
28 | name: 'Bottom'
29 | }
30 | ],
31 | callback: function(data) {
32 | console.log(data);
33 | // Example { Top: 5, Middle: 3 }
34 | }
35 | });
36 | ```
37 |
38 | ## Options
39 | #### `fields` array (required)
40 | An array of object listing the DOM elements you want to track. Each object should specify a `selector` property and a `name` property.
41 |
42 | #### `reportInterval` number
43 | The interval, in seconds, used to issue a report. The default is 10 seconds.
44 |
45 | #### `percentOnScreen` string
46 | This determines what percentage of the field must be on screen for it to be considered in view. The default is `50%`. One exception to this rule: if a field occupies the majority of the viewport it will be considered in view regardless of its viewable percentage.
47 |
48 | #### `googleAnalytics` boolean
49 | Setting this to true (default is false) will send a Google Analytics event for each field (if the field has data to report) when the report is issued.
50 |
51 | #### `callback` function
52 | The callback function that receives the screentime data.
53 |
54 | ## Methods
55 | #### `reset`
56 | Calling `$.screentime.reset()` will reset the counter. This is useful for Single Page Applications where the page context is reloaded client side.
57 |
58 | ## Posting the data
59 | The built-in Google Analytics option is an easy way to quickly start collecting screentime data, but it's not exactly scalable. For each field that has data to report, a separate GA Event has to be sent. If you're only tracking 2 or 3 fields, this is probably fine. But anything more than that and you might start hitting the [GA collection limit](https://developers.google.com/analytics/devguides/collection/gajs/limits-quotas).
60 |
61 | For scalabla data collection you'll want to provide your own backend or use a service like Keen IO. In the examples folder there's a demo showing how easy it is to use Keen for this. It's as simple as this:
62 |
63 | ```javascript
64 | ...
65 | callback: function(data) {
66 | Keen.addEvent("screentime", data);
67 | }
68 | ...
69 | ```
70 |
71 |
72 |
--------------------------------------------------------------------------------
/examples/keen-io.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Screentime Example - Keen IO
6 |
16 |
17 |
18 | Screentime Test Page
19 | #top
20 | #middle
21 | #bottom
22 | #end
23 |
24 |
25 |
26 |
44 |
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "screentime",
3 | "version": "0.2.0",
4 | "description": "An analytics plugin to measure how much time elements spend on-screen.",
5 | "main": "screentime.js",
6 | "directories": {
7 | "example": "examples",
8 | "test": "test"
9 | },
10 | "scripts": {
11 | "test": "echo \"Error: no test specified\" && exit 1"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/robflaherty/screentime.git"
16 | },
17 | "keywords": [
18 | "analytics",
19 | "google analytics",
20 | "time"
21 | ],
22 | "author": "Rob Flaherty",
23 | "license": "MIT",
24 | "bugs": {
25 | "url": "https://github.com/robflaherty/screentime/issues"
26 | },
27 | "homepage": "http://screentime.parsnip.io/",
28 | "dependencies": {
29 | "jquery": ">=1.8"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/screentime.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * @preserve
3 | * Screentime.js | v0.2.0
4 | * Copyright (c) 2016 Rob Flaherty (@robflaherty)
5 | * Licensed under the MIT and GPL licenses.
6 | */
7 |
8 | /* Universal module definition */
9 |
10 | (function(factory) {
11 | if (typeof define === 'function' && define.amd) {
12 | // AMD
13 | define(['jquery'], factory);
14 | } else if (typeof module === 'object' && module.exports) {
15 | // CommonJS
16 | module.exports = factory(require('jquery'));
17 | } else {
18 | // Browser globals
19 | factory(jQuery);
20 | }
21 | }(function($) {
22 |
23 | /* Screentime */
24 |
25 | var defaults = {
26 | fields: [],
27 | percentOnScreen: '50%',
28 | reportInterval: 10,
29 | googleAnalytics: false,
30 | callback: function(){}
31 | };
32 |
33 | $.screentime = function(options) {
34 | options = $.extend({}, defaults, options);
35 |
36 | // Convert perecent string to number
37 | options.percentOnScreen = parseInt(options.percentOnScreen.replace('%', ''), 10);
38 |
39 | var counter = {};
40 | var cache = {};
41 | var log = {};
42 | var looker = null;
43 | var started = false;
44 | var universalGA, classicGA;
45 |
46 | if (!options.fields.length) {
47 | return;
48 | }
49 |
50 | if (options.googleAnalytics) {
51 |
52 | if (typeof ga === "function") {
53 | universalGA = true;
54 | }
55 |
56 | if (typeof _gaq !== "undefined" && typeof _gaq.push === "function") {
57 | classicGA = true;
58 | }
59 |
60 | }
61 |
62 | /*
63 | * Utilities
64 | */
65 |
66 | /*!
67 | * visibly - v0.6 Aug 2011 - Page Visibility API Polyfill
68 | * http://github.com/addyosmani
69 | * Copyright (c) 2011 Addy Osmani
70 | * Dual licensed under the MIT and GPL licenses.
71 | *
72 | * Methods supported:
73 | * visibly.onVisible(callback)
74 | * visibly.onHidden(callback)
75 | * visibly.hidden()
76 | * visibly.visibilityState()
77 | * visibly.visibilitychange(callback(state));
78 | */
79 |
80 | (function(){window.visibly={q:document,p:undefined,prefixes:["webkit","ms","o","moz","khtml"],props:["VisibilityState","visibilitychange","Hidden"],m:["focus","blur"],visibleCallbacks:[],hiddenCallbacks:[],genericCallbacks:[],_callbacks:[],cachedPrefix:"",fn:null,onVisible:function(i){if(typeof i=="function"){this.visibleCallbacks.push(i)}},onHidden:function(i){if(typeof i=="function"){this.hiddenCallbacks.push(i)}},getPrefix:function(){if(!this.cachedPrefix){for(var i=0;b=this.prefixes[i++];){if(b+this.props[2]in this.q){this.cachedPrefix=b;return this.cachedPrefix}}}},visibilityState:function(){return this._getProp(0)},hidden:function(){return this._getProp(2)},visibilitychange:function(i){if(typeof i=="function"){this.genericCallbacks.push(i)}var t=this.genericCallbacks.length;if(t){if(this.cachedPrefix){while(t--){this.genericCallbacks[t].call(this,this.visibilityState())}}else{while(t--){this.genericCallbacks[t].call(this,arguments[0])}}}},isSupported:function(i){return this.cachedPrefix+this.props[2]in this.q},_getProp:function(i){return this.q[this.cachedPrefix+this.props[i]]},_execute:function(i){if(i){this._callbacks=i==1?this.visibleCallbacks:this.hiddenCallbacks;var t=this._callbacks.length;while(t--){this._callbacks[t]()}}},_visible:function(){window.visibly._execute(1);window.visibly.visibilitychange.call(window.visibly,"visible")},_hidden:function(){window.visibly._execute(2);window.visibly.visibilitychange.call(window.visibly,"hidden")},_nativeSwitch:function(){this[this._getProp(2)?"_hidden":"_visible"]()},_listen:function(){try{if(!this.isSupported()){if(this.q.addEventListener){window.addEventListener(this.m[0],this._visible,1);window.addEventListener(this.m[1],this._hidden,1)}else{if(this.q.attachEvent){this.q.attachEvent("onfocusin",this._visible);this.q.attachEvent("onfocusout",this._hidden)}}}else{this.q.addEventListener(this.cachedPrefix+this.props[1],function(){window.visibly._nativeSwitch.apply(window.visibly,arguments)},1)}}catch(i){}},init:function(){this.getPrefix();this._listen()}};this.visibly.init()})();
81 |
82 | function random() {
83 | return Math.round(Math.random() * 2147483647);
84 | }
85 |
86 | /*
87 | * Constructors
88 | */
89 |
90 | function Field(elem) {
91 | this.selector = elem.selector;
92 | $elem = this.$elem = $(elem.selector);
93 | this.name = elem.name;
94 |
95 | this.top = $elem.offset().top;
96 | this.height = $elem.height();
97 | this.bottom = this.top + this.height;
98 | this.width = $elem.width();
99 | }
100 |
101 | function Viewport() {
102 | var $window = $(window);
103 |
104 | this.top = $window.scrollTop();
105 | this.height = $window.height();
106 | this.bottom = this.top + this.height;
107 | this.width = $window.width();
108 | }
109 |
110 | /*
111 | * Do Stuff
112 | */
113 |
114 | function sendGAEvent(field, time) {
115 |
116 | if (universalGA) {
117 | ga('send', 'event', 'Screentime', 'Time on Screen', field, parseInt(time, 10), {'nonInteraction': true});
118 | }
119 |
120 | if (classicGA) {
121 | _gaq.push(['_trackEvent', 'Screentime', 'Time on Screen', field, parseInt(time, 10), true]);
122 | }
123 |
124 | }
125 |
126 | function onScreen(viewport, field) {
127 | var cond, buffered, partialView;
128 |
129 | // Field not hidden by design
130 | if($(field.selector).is(':hidden') ){
131 | return false;
132 | }
133 |
134 | // Field entirely within viewport
135 | if ((field.bottom <= viewport.bottom) && (field.top >= viewport.top)) {
136 | return true;
137 | }
138 |
139 | // Field bigger than viewport
140 | if (field.height > viewport.height) {
141 |
142 | cond = (viewport.bottom - field.top) > (viewport.height / 2) && (field.bottom - viewport.top) > (viewport.height / 2);
143 |
144 | if (cond) {
145 | return true;
146 | }
147 |
148 | }
149 |
150 | // Partially in view
151 | buffered = (field.height * (options.percentOnScreen/100));
152 | partialView = ((viewport.bottom - buffered) >= field.top && (field.bottom - buffered) > viewport.top);
153 |
154 | return partialView;
155 |
156 | }
157 |
158 | function checkViewport() {
159 | var viewport = new Viewport();
160 |
161 | $.each(cache, function(key, val) {
162 | if (onScreen(viewport, val)) {
163 | log[key] += 1;
164 | counter[key] += 1;
165 | }
166 | });
167 |
168 | }
169 |
170 | function report() {
171 |
172 | var data = {};
173 |
174 | $.each(counter, function(key, val) {
175 | if (val > 0) {
176 | data[key] = val;
177 | counter[key] = 0;
178 |
179 | if (options.googleAnalytics) {
180 | sendGAEvent(key, val);
181 | }
182 |
183 | }
184 | });
185 |
186 | if (!$.isEmptyObject(data)) {
187 | options.callback.call(this, data, log);
188 | }
189 |
190 | }
191 |
192 | function startTimers() {
193 |
194 | if (!started) {
195 | checkViewport();
196 | started = true;
197 | }
198 |
199 | looker = setInterval(function() {
200 | checkViewport();
201 | }, 1000);
202 |
203 | reporter = setInterval(function() {
204 | report();
205 | }, options.reportInterval * 1000);
206 |
207 | }
208 |
209 | function stopTimers() {
210 | clearInterval(looker);
211 | clearInterval(reporter);
212 | }
213 |
214 | $.screentime.reset = function() {
215 | stopTimers();
216 |
217 | $.each(cache, function(key, val) {
218 | log[key] = 0;
219 | counter[key] = 0;
220 | });
221 |
222 | startTimers();
223 | }
224 |
225 | function init() {
226 |
227 | $.each(options.fields, function(index, elem) {
228 | if ($(elem.selector).length) {
229 | var field = new Field(elem);
230 | cache[field.name] = field;
231 | counter[field.name] = 0;
232 | log[field.name] = 0;
233 | }
234 | });
235 |
236 | startTimers();
237 |
238 | visibly.onHidden(function() {
239 | stopTimers();
240 | });
241 |
242 | visibly.onVisible(function() {
243 | stopTimers();
244 | startTimers();
245 | });
246 |
247 | }
248 |
249 | init();
250 |
251 | };
252 |
253 | }));
254 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Screentime Test
6 |
20 |
21 |
22 | Screentime Test Page
23 | #top
24 | #middle
25 | #bottom
26 | #end
27 |
28 |
29 |
54 |
55 |
--------------------------------------------------------------------------------