├── .gitignore ├── LICENSE ├── README.md ├── lib ├── WebSessionCounter.js └── index.js ├── package.json └── src ├── WebSessionCounter.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Swizec Teller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Web Session Counter 3 | 4 | What do you do if your boss comes up to you and asks *"So how many 5 | times must a person come to our app before they give us $500?"* 6 | 7 | As an engineer you might not care, but this is the stuff that keeps 8 | your CEO and growth lead and head of product up at night. A business 9 | owner that can answer that 👆 question reliably is god amongst men. 10 | 11 | You can use this repo :) 12 | 13 | ## How to use 14 | 15 | ``` 16 | $ npm install --save web-session-counter 17 | ``` 18 | 19 | ```javascript 20 | 21 | import WebSessionCounter from 'web-session-counter'; 22 | 23 | // Do this on user activity 24 | WebSessionCounter.update(); 25 | 26 | // To get the total count of sessions 27 | const count = WebSessionCounter.count; 28 | 29 | ``` 30 | 31 | `.update()` is called automatically every time you import 32 | WebSessionCounter. I recommend calling `.update()`, if you have a 33 | single page app that doesn't perform a lot of refreshes. Calling 34 | `.update` frequently, ensures your code will correctly detect every 30 35 | minute period of inactivity. 36 | 37 | ## What is a session 38 | 39 | We use the [same definition as Google Analytics](https://support.google.com/analytics/answer/2731565?hl=en). The tl;dr is that a new session starts after every: 40 | 41 | - 30 minutes of inactivity 42 | - midnight 43 | - `utm_campaign` query change 44 | 45 | 46 | Enjoy. 47 | -------------------------------------------------------------------------------- /lib/WebSessionCounter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 8 | 9 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 10 | 11 | var _querystring = require('querystring'); 12 | 13 | var _querystring2 = _interopRequireDefault(_querystring); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 18 | 19 | function isLocalStorageSupported() { 20 | var testKey = 'test', 21 | storage = window.localStorage; 22 | try { 23 | storage.setItem(testKey, '1'); 24 | storage.removeItem(testKey); 25 | return true; 26 | } catch (error) { 27 | return false; 28 | } 29 | } 30 | 31 | var canUseLocalStorage = isLocalStorageSupported(); 32 | 33 | var WebSessionCounter = function () { 34 | function WebSessionCounter() { 35 | _classCallCheck(this, WebSessionCounter); 36 | 37 | this.update(); 38 | } 39 | 40 | _createClass(WebSessionCounter, [{ 41 | key: 'update', 42 | value: function update() { 43 | if (canUseLocalStorage) { 44 | var count = this.count, 45 | time = this.lastActive; 46 | 47 | if (count === 0 || this.isNewSession()) { 48 | this.count = count + 1; 49 | this.lastActive = new Date(); 50 | this.lastUtmCampaign = this.currentUtmCampaign; 51 | } 52 | } 53 | } 54 | }, { 55 | key: 'isNewSession', 56 | value: function isNewSession() { 57 | // use definition from https://support.google.com/analytics/answer/2731565?hl=en 58 | 59 | var time = this.lastActive, 60 | now = new Date(); 61 | 62 | return [(now - time) / 1000 / 60 > 30, now.toDateString() !== time.toDateString(), this.lastUtmCampaign !== this.currentUtmCampaign].some(function (b) { 63 | return b; 64 | }); 65 | } 66 | }, { 67 | key: 'count', 68 | get: function get() { 69 | if (canUseLocalStorage) { 70 | return Number(window.localStorage.getItem('user_web_session_count')); 71 | } else { 72 | return NaN; 73 | } 74 | }, 75 | set: function set(val) { 76 | window.localStorage.setItem('user_web_session_count', val); 77 | } 78 | }, { 79 | key: 'lastActive', 80 | get: function get() { 81 | var time = window.localStorage.getItem('user_web_session_last_active'); 82 | 83 | if (time) { 84 | return new Date(time); 85 | } else { 86 | return new Date(); 87 | } 88 | }, 89 | set: function set(time) { 90 | window.localStorage.setItem('user_web_session_last_active', time.toISOString()); 91 | } 92 | }, { 93 | key: 'lastUtmCampaign', 94 | get: function get() { 95 | return window.localStorage.getItem('user_web_session_utm_campaign'); 96 | }, 97 | set: function set(val) { 98 | window.localStorage.setItem('user_web_session_utm_campaign', val); 99 | } 100 | }, { 101 | key: 'currentUtmCampaign', 102 | get: function get() { 103 | var _window$location$href = window.location.href.split('?'), 104 | _window$location$href2 = _slicedToArray(_window$location$href, 2), 105 | path = _window$location$href2[0], 106 | _window$location$href3 = _window$location$href2[1], 107 | query = _window$location$href3 === undefined ? '' : _window$location$href3, 108 | _querystring$parse = _querystring2.default.parse(query), 109 | _querystring$parse$ut = _querystring$parse.utm_campaign, 110 | utm_campaign = _querystring$parse$ut === undefined ? '' : _querystring$parse$ut; 111 | 112 | return utm_campaign; 113 | } 114 | }]); 115 | 116 | return WebSessionCounter; 117 | }(); 118 | 119 | exports.default = new WebSessionCounter(); -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _WebSessionCounter = require('./WebSessionCounter'); 8 | 9 | Object.defineProperty(exports, 'default', { 10 | enumerable: true, 11 | get: function get() { 12 | return _interopRequireDefault(_WebSessionCounter).default; 13 | } 14 | }); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-session-counter", 3 | "version": "1.1.0", 4 | "description": "Utility to count a user's web sessions based on the definition GA uses. Edit", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "babel src -d lib", 9 | "preversion": "", 10 | "version": "npm run build && git add -A lib", 11 | "postversion": "git push && git push --tags && rm -rf build/temp" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Swizec/web-session-counter.git" 16 | }, 17 | "keywords": [ 18 | "sessions", 19 | "analytics", 20 | "users", 21 | "es6" 22 | ], 23 | "author": "Swizec Teller", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/Swizec/web-session-counter/issues" 27 | }, 28 | "homepage": "https://github.com/Swizec/web-session-counter#readme", 29 | "dependencies": { 30 | "querystring": "^0.2.0" 31 | }, 32 | "babel": { 33 | "presets": [ 34 | "latest" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/WebSessionCounter.js: -------------------------------------------------------------------------------- 1 | 2 | import querystring from 'querystring'; 3 | 4 | function isLocalStorageSupported() { 5 | let testKey = 'test', storage = window.localStorage; 6 | try { 7 | storage.setItem(testKey, '1'); 8 | storage.removeItem(testKey); 9 | return true; 10 | } 11 | catch (error) { 12 | return false; 13 | } 14 | } 15 | 16 | const canUseLocalStorage = isLocalStorageSupported(); 17 | 18 | class WebSessionCounter { 19 | constructor() { 20 | this.update(); 21 | } 22 | 23 | get count() { 24 | if (canUseLocalStorage) { 25 | return Number(window.localStorage.getItem('user_web_session_count')); 26 | }else{ 27 | return NaN; 28 | } 29 | } 30 | 31 | set count(val) { 32 | window.localStorage.setItem('user_web_session_count', val); 33 | } 34 | 35 | get lastActive() { 36 | const time = window.localStorage.getItem('user_web_session_last_active'); 37 | 38 | if (time) { 39 | return new Date(time); 40 | }else{ 41 | return new Date(); 42 | } 43 | } 44 | 45 | set lastActive(time) { 46 | window.localStorage.setItem('user_web_session_last_active', time.toISOString()); 47 | } 48 | 49 | get lastUtmCampaign() { 50 | return window.localStorage.getItem('user_web_session_utm_campaign'); 51 | } 52 | 53 | set lastUtmCampaign(val) { 54 | window.localStorage.setItem('user_web_session_utm_campaign', val); 55 | } 56 | 57 | get currentUtmCampaign() { 58 | const [ path, query = '' ] = window.location.href.split('?'), 59 | { utm_campaign = '' } = querystring.parse(query); 60 | 61 | return utm_campaign; 62 | } 63 | 64 | update() { 65 | if (canUseLocalStorage) { 66 | let count = this.count, 67 | time = this.lastActive; 68 | 69 | if (count === 0 || this.isNewSession()) { 70 | this.count = count + 1; 71 | this.lastActive = new Date(); 72 | this.lastUtmCampaign = this.currentUtmCampaign; 73 | } 74 | } 75 | } 76 | 77 | isNewSession() { 78 | // use definition from https://support.google.com/analytics/answer/2731565?hl=en 79 | 80 | const time = this.lastActive, 81 | now = new Date(); 82 | 83 | return [ 84 | (now - time)/1000/60 > 30, 85 | now.toDateString() !== time.toDateString(), 86 | this.lastUtmCampaign !== this.currentUtmCampaign 87 | ].some(b => b); 88 | } 89 | } 90 | 91 | export default new WebSessionCounter(); 92 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | export { default } from './WebSessionCounter'; 3 | --------------------------------------------------------------------------------